这回的博客继续把 React Hooks 的相关内容整理整理,估计 Hooks 的内容可以写个三四篇。今天算是忙里偷闲了,如果在工位上确实没有其他要紧的事情,写博客倒是一个不错的选择,毕竟其他的人不清楚情况,比纯粹的摸鱼看起来要好得多了。不知道这样的状态可以持续多久,也算是非常难得了。
本篇博客主要讲两个 Hooks,useReducer 和 useContext。这两种 Hooks 都与组件的状态管理有关,这次一齐归纳。其余的 Hooks 在后续博客进行整理。
useReducer - useState 的升级操作
useReducer 可以视为是 useState 的升级操作,Hooks 借此践行 Flux / Redux 的思想。在之前的博客里已经对 Redux 进行了介绍,Redux 也是一种状态容器。如果对 Redux 有一些了解,就更容易明白 useReducer 是如何工作的了。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。下面是一个简单的 useReducer 使用案例:
const initialState = {count: 0}; // 创建 state 的初始值 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, initialState); // 传给 useReducer,得到读写 API return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
初始化 state 的方式有两种,上面例子中的,将初始 state 作为第二个参数传入 useReducer 是最简单的方法。此外,还可以将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialState)。这么做可以将用于计算 state 的初始值的逻辑提取到组件外部(与 useState 接受函数初始值的目的类似),reducer 重置 state 也更为方便。下面是一个例子:
function init(initialCount) { // 初始值操作函数 return {count: initialCount}; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); // 返回的值正好是 state 的初始值 default: throw new Error(); } } function Counter({initialCount}) { // 有第三个函数的情形下,useReducer 第二个参数就不是 state 的初始值了 const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
总得来说,useReducer 是 useState 的复杂版,但是在某些场景下,useReducer 会比 useState 更适用。例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。一般来说,如果有多个 state 需要放在一个对象里,这个时候用 useReducer 更为直观。需要注意的是,在上一篇博客中提到的 useState 不能「局部更新」对象,在 useReducer 里也是不行的。
useContext - 上下文
useContext 的字面意思是「使用上下文」,「上下文」又是什么呢?学 this 的时候知道 this 就是上下文,全局变量就是全局作用域下的上下文,所以 useContext 就是让组件之间也能有「局部的全局变量」。虽然在之前函数组件中并不是不能使用 React Context API,但是 useContext 让组件使用 context 更加方便。React Context API 的相关内容过一段时间再写博客来介绍了,下面先来看看使用 useContext 的一个简单例子:
const themes = { light: { foreground: "#000000", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#222222" } }; const ThemeContext = React.createContext(themes.light); // 这里传递的参数是 value 的默认值 function App() { return ( <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar(props) { // 中间组件 return ( <div> <ThemedButton /> </div> ); } function ThemedButton() { // 需要用到 context 的组件 const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }
useContext 接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。
useContext(MyContext) 相当于 class 组件中的static contextType = MyContext
或者<MyContext.Consumer>
。useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
context 值由上层组件中距离当前组件最近的<ThemeContext.Provider>
的value
prop 决定。<ThemeContext.Provider>
则时用以圈定 context 的作用域,作用于内的所有组件都可以通过使用 useContext 操作 context。调用了 useContext 的组件总会在 context 值变化时重新渲染。context 的改变不是响应式的,是从上往下组件逐级通知的过程,而不是直接通知到某个组件。
上文提到的 useReducer 创建的状态只能在其本身的函数组件中使用,但是结合 useContext 之后,就可以让 useReducer 代替 Redux 了。下面是一个使用 useReducer 模拟 Redux 的例子:
const initialState = {count: 0}; 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(); } } const ThemeContext = React.createContext(initialState); function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <ThemeContext.Provider value={{state, dispatch}}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar(props) { const { state } = useContext(ThemeContext); return ( <div> {state} <AddButton /> <SubButton /> </div> ); } function AddButton() { const { dispatch } = useContext(ThemeContext); return ( <button onClick={() => dispatch({type: 'increment'})}> 增加 </button> ); } function SubButton() { const { dispatch } = useContext(ThemeContext); return ( <button onClick={() => dispatch({type: 'decrement'})}> 减少 </button> ); }
大致来说,useReducer 模拟 Redux 的实现过程可以概括为以下步骤:
- 将数据集中在 store;
- 将操作几种在 reducer;
- 创建一个 Context;
- 用 useReducer 创建对数据的读写 API;
- 将第四步的内容放到第三步的 Context;
- 用 Context.Provider 将 Context 提供给所有组件;
- 各个组件用 useContext 获取读写 API。
以上就是本篇博客中关于 useReducer 和 useContext 的所有内容。写完这篇博客,我心里是很高兴的,好久没有试过两条连续写博客了。但也是很无语的,因为如果不是这段时间特殊时期,自己都不知道套拖到什么时候才来看这些 Hooks 的课程,写这篇博客更是遥遥无期。虽然放了一个月假,也算是塞翁失马,焉知非福了。以后还是要抽空多看看课程,给自己的博客加点存量了。