学习 Hooks【Plan - June - Week 2】
一、React API
React 提供了丰富的核心 API,用于创建组件、管理状态、处理副作用、优化性能等。本文档总结 React 常用的 API 方法和组件。
1. React 核心 API
React.createElement(type, props, …children)
- 用于创建 React 元素,JSX 会被编译成该函数调用。
type
:标签名或组件函数props
:属性对象children
:子节点列表
React.Component
- React 组件基类,用于创建类组件。
- 语法:
class MyComponent extends React.Component {render() {return <div>Hello</div>;}
}
React.PureComponent
- 类组件基类,带浅层比较的性能优化。
- 如果 props 和 state 没有变化,不会重新渲染。
React.Fragment
- 用于返回多个子元素的容器,不会产生额外的 DOM 节点。
- 语法:
<React.Fragment><Child1 /><Child2 />
</React.Fragment>
或者简写为:
<><Child1 /><Child2 />
</>
2. React Hooks API
useState(initialState)
- 声明状态变量,返回
[state, setState]
。
useEffect(effect, deps)
- 用于副作用操作,类似生命周期。
deps
数组决定何时重新执行副作用。
useContext(Context)
- 订阅 React Context。
useReducer(reducer, initialState)
- 管理复杂状态,类似 Redux。
useMemo(factory, deps)
- 缓存计算结果,避免重复计算。
useCallback(callback, deps)
- 缓存函数引用,避免子组件不必要渲染。
useRef(initialValue)
- 创建可变引用对象,常用于获取 DOM 或存储变量。
useImperativeHandle(ref, createHandle, deps)
- 自定义暴露给父组件的实例值。
useLayoutEffect(effect, deps)
- 与 useEffect 类似,但在 DOM 变更后同步触发。
3. 辅助工具函数
React.cloneElement(element, [props], […children])
- 克隆并修改已有 React 元素。
React.isValidElement(object)
- 判断对象是否是有效的 React 元素。
React.Children
- 工具对象,操作
props.children
:React.Children.map
React.Children.forEach
React.Children.count
React.Children.only
React.Children.toArray
4. 其他重要 API
React.createContext(defaultValue)
- 创建上下文(Context)。
React.forwardRef(renderFunction)
- 转发 ref,允许父组件访问子组件 DOM。
React.memo(Component, [areEqual])
- 组件的性能优化,高阶组件,类似 PureComponent。
React.lazy(factory)
- 支持代码分割,懒加载组件。
React.Suspense
- 配合 React.lazy 使用,显示加载状态。
二、useEffect
useEffect
是 React 中管理副作用(side effects)的 Hook。副作用包括数据获取、订阅、手动 DOM 操作等。
1. 基础用法
import React, { useEffect, useState } from 'react';function Example() {const [count, setCount] = useState(0);useEffect(() => {// 每次渲染后执行副作用document.title = `你点击了 ${count} 次`;});return (<div><p>你点击了 {count} 次</p><button onClick={() => setCount(count + 1)}>点击我</button></div>);
}
2. 参数说明
useEffect(effect: () => (void | (() => void)), deps?: DependencyList)
effect
:副作用函数,函数内部可以执行副作用代码,且可返回清理函数。deps
:依赖数组(可选),用于控制副作用执行的时机。
3. 依赖数组
- 无依赖数组:副作用在每次组件渲染后执行。
- 空依赖数组 (
[]
):副作用只在组件挂载(Mount)和卸载(Unmount)时执行一次。 - 有依赖项数组:只有当依赖项发生变化时,副作用才执行。
示例:
useEffect(() => {console.log('只在挂载和卸载时运行');
}, []);useEffect(() => {console.log('count 发生变化时运行');
}, [count]);
4. 清理副作用
effect
函数可以返回一个清理函数,在组件卸载或依赖更新前调用。
useEffect(() => {const id = setInterval(() => {console.log('定时器运行中');}, 1000);// 返回清理函数,清除定时器return () => clearInterval(id);
}, []);
5. 副作用示例
- 数据请求(fetch)
- 事件监听和解绑
- 订阅和取消订阅
- 手动 DOM 操作
- 设置定时器
6. 注意事项
- 避免无限循环:确保依赖数组正确,防止副作用无限触发。
- 同步 vs 异步:
useEffect
不支持直接声明为async
函数,但可在内部调用异步函数。
示例:
useEffect(() => {async function fetchData() {const res = await fetch('/api/data');const data = await res.json();// 处理数据}fetchData();
}, []);
7. useEffect 与生命周期对应关系
生命周期方法 | useEffect 变体 |
---|---|
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}) |
componentWillUnmount | useEffect(() => { return () => {} }, []) |
8. React 18+ 特别说明
React 18 开启严格模式下,useEffect
会在开发环境中执行两次以帮助发现副作用问题。
9. 示例完整代码
import React, { useState, useEffect } from 'react';function Timer() {const [count, setCount] = useState(0);useEffect(() => {const timerId = setInterval(() => {setCount(c => c + 1);}, 1000);return () => clearInterval(timerId);}, []);return <h1>计时器:{count} 秒</h1>;
}
三、useContext
useContext
用于在函数组件中访问 React Context 的值,简化了跨组件传递数据的过程。
1. 什么是 Context?
- Context 提供了一种在组件树中传递数据的方法,无需通过每一级组件的 props。
- 适用于主题、语言、认证信息等全局数据。
2. useContext 的用法
const value = useContext(MyContext);
MyContext
是通过React.createContext
创建的 Context 对象。useContext
返回当前 Context 的值,即最近的<MyContext.Provider>
所提供的值。- 当 Provider 的 value 改变时,组件会重新渲染。
3. 创建 Context 示例
import React, { createContext, useContext } from 'react';// 创建 Context
const ThemeContext = createContext('light');function Toolbar() {return (<div><ThemedButton /></div>);
}function ThemedButton() {// 读取 Context 值const theme = useContext(ThemeContext);return <button style={{ background: theme === 'dark' ? '#333' : '#ccc' }}>按钮</button>;
}function App() {return (// 提供 Context 值<ThemeContext.Provider value="dark"><Toolbar /></ThemeContext.Provider>);
}
4. 注意事项
- 组件必须在对应 Context 的 Provider 内部,否则使用默认值。
- 只有当 Context 的值发生变化时,使用该 Context 的组件才会重新渲染。
- 不要在组件内直接修改 Context 的值,应通过 Provider 传递新的值。
5. 常见使用场景
- 主题切换(light/dark)
- 用户认证信息
- 国际化语言设置
- 全局配置参数
6. 组合多个 Context
- 可以多次调用
useContext
读取多个不同的 Context。
const theme = useContext(ThemeContext);
const user = useContext(UserContext);
7. 对比 Context.Consumer
useContext
更简洁,适用于函数组件。- 类组件中仍可用
<Context.Consumer>
读取 Context。
四、用 Reducer 和 Context 扩展状态管理
当应用变复杂,单纯用
useState
管理状态变得笨重。此时可以使用useReducer
管理复杂状态逻辑,结合Context
实现跨组件共享状态,替代 Redux 等库。
1. 为什么用 Reducer?
- 多个状态相互关联,修改逻辑复杂
- 需要明确状态变化的过程和原因(动作)
- 状态逻辑集中,更易维护和测试
2. useReducer 简介
const [state, dispatch] = useReducer(reducer, initialState);
state
:当前状态dispatch
:派发动作(action)reducer
:状态变更函数(state, action) => newState
示例:计数器
function reducer(state, action) {switch (action.type) {case 'increment':return { count: state.count + 1 };case 'decrement':return { count: state.count - 1 };default:throw new Error();}
}function Counter() {const [state, dispatch] = useReducer(reducer, { count: 0 });return (<>Count: {state.count}<button onClick={() => dispatch({ type: 'increment' })}>+1</button><button onClick={() => dispatch({ type: 'decrement' })}>-1</button></>);
}
3. 结合 Context 实现跨组件状态共享
- 将
state
和dispatch
放入 Context,供多个组件访问。 - 这样,父组件可以集中管理状态,子组件通过
useContext
访问和派发动作。
const CountContext = React.createContext();function CounterProvider({ children }) {const [state, dispatch] = useReducer(reducer, { count: 0 });return (<CountContext.Provider value={{ state, dispatch }}>{children}</CountContext.Provider>);
}function CounterDisplay() {const { state } = React.useContext(CountContext);return <div>Count: {state.count}</div>;
}function CounterButtons() {const { dispatch } = React.useContext(CountContext);return (<><button onClick={() => dispatch({ type: 'increment' })}>+1</button><button onClick={() => dispatch({ type: 'decrement' })}>-1</button></>);
}
4. 应用结构示例
function App() {return (<CounterProvider><CounterDisplay /><CounterButtons /></CounterProvider>);
}
5. 优缺点
优点 | 缺点 |
---|---|
明确的状态管理流程和结构 | 代码稍显复杂,学习曲线陡峭 |
集中管理状态,方便维护与测试 | 过度使用可能导致冗余 |
方便实现复杂状态变化和回退等功能 | 状态共享时 Context 过度更新会导致性能问题 |
6. 最佳实践
- 只对需要共享的状态使用 Context 和 Reducer
- 将逻辑拆分成多个 reducer 和 Context(模块化)
- 使用 React.memo、useMemo 优化性能
- 明确 Action 类型和 Payload,写清楚 Reducer 逻辑
以下是 React 官方文档 《井字棋教程 (Tic Tac Toe Tutorial)》 的详细 Markdown 学习笔记整理:
五、React 井字棋教程
通过构建一个经典的井字棋游戏,学习 React 基础知识,包括组件设计、状态管理、事件处理、以及提升组件能力。
1. 项目介绍
- 井字棋是一个简单的 3 x 3 网格游戏,两名玩家轮流放置 X 和 O。
- 目标是先在水平、垂直或对角线上连成一条线。
- 教程通过这个项目介绍 React 的核心概念。
2. 构建游戏的步骤
- 创建一个
Square
组件,表示棋盘中的一个格子。 - 创建一个
Board
组件,包含 9 个Square
。 - 维护棋盘状态,响应用户点击。
- 判断胜负逻辑。
- 添加游戏历史记录,实现时间旅行功能。
3. 组件设计
Square 组件
- 功能:显示一个按钮,表示一个格子。
- 接收 props:
value
(X, O 或 null),onClick
点击事件处理。
function Square({ value, onClick }) {return (<button className="square" onClick={onClick}>{value}</button>);
}
Board 组件
- 维护 9 个格子的状态。
- 渲染 9 个
Square
,传入对应的value
和onClick
。
function Board() {const [squares, setSquares] = React.useState(Array(9).fill(null));function handleClick(i) {const nextSquares = squares.slice();nextSquares[i] = 'X';setSquares(nextSquares);}return (<div><div className="board-row"><Square value={squares[0]} onClick={() => handleClick(0)} />{/* 其他格子 */}</div>{/* 其他行 */}</div>);
}
4. 状态提升(Lifting State Up)
- 当需要多个组件共享状态时,将状态提升到它们最近的共同父组件。
- 在教程中,
Board
组件的状态被提升到Game
组件管理。 Game
组件管理历史状态,实现回退功能。
5. 计算游戏胜负
- 实现一个函数
calculateWinner(squares)
,判断当前棋盘是否有玩家获胜。 - 如果获胜,显示胜者信息,游戏结束。
function calculateWinner(squares) {const lines = [[0,1,2], [3,4,5], [6,7,8],[0,3,6], [1,4,7], [2,5,8],[0,4,8], [2,4,6]];for (let line of lines) {const [a,b,c] = line;if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {return squares[a];}}return null;
}
6. 处理用户交互
- 点击格子时更新状态,切换玩家。
- 禁止点击已被占用的格子。
- 游戏结束后禁止继续点击。
7. 时间旅行(历史记录)
Game
组件维护棋盘的历史数组。- 用户可点击历史按钮,回到之前的任一步骤。
- 利用数组和状态管理,实现“时间旅行”效果。
function Game() {const [history, setHistory] = React.useState([Array(9).fill(null)]);const [stepNumber, setStepNumber] = React.useState(0);const [xIsNext, setXIsNext] = React.useState(true);function handleClick(i) {const historyUpToStep = history.slice(0, stepNumber + 1);const current = historyUpToStep[historyUpToStep.length - 1];const squares = current.slice();if (calculateWinner(squares) || squares[i]) return;squares[i] = xIsNext ? 'X' : 'O';setHistory([...historyUpToStep, squares]);setStepNumber(historyUpToStep.length);setXIsNext(!xIsNext);}function jumpTo(step) {setStepNumber(step);setXIsNext((step % 2) === 0);}// 渲染历史按钮,调用 jumpTo
}
学习资料来源
React 参考
useEffect
useContext
使用 Reducer 和 Context
教程:井字棋游戏