这回的博客继续把 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 的课程,写这篇博客更是遥遥无期。虽然放了一个月假,也算是塞翁失马,焉知非福了。以后还是要抽空多看看课程,给自己的博客加点存量了。

雪花