React解密:useReducer()的用法及优势

2,708 阅读3分钟

定义

useState() 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前 state 以及与之配套的 dispatch 方法

用法

const [state, dispatch] = useReducer(reducer, initialArg, init);

使用场景

  • 当多个 state 需要一起更新时
  • 当state 更新逻辑较复杂
  • 当下一个 state 依赖于之前的 state,即 编写 setState(prevState => newState)时 包括但不限于以上三种。

在以上场景中使用时,useReducer()相对于 useState() 的优势

  1. 上述场景中,使用 useReducer()的好处分析:

    • reducer 相对于 useState 可以更好的描述“如何更新状态”。 比如:reducer 能够读取相关的状态、同时更新多个状态。
    • 【组件负责发出 action,reducer 负责更新状态】的解耦模式, 使得代码逻辑更加清晰。代码行为更加可以预测(比如:useEffect 的更新时机更加稳定)
    • 通过传递 dispatch ,可以减少状态值的传递。 useReducer 总是返回相同的 dispatch 函数,这是彻底解耦的标志:状态更新逻辑可以任意变化,而发起 action 的渠道始终不变。
    • 因前面的解耦模式,useEffect 函数体、callback function 只需要使用 dispatch 来发出 action ,而无需直接依赖状态值。因此,useEffectuseCallbackuseMemodeps数组中无需包含状态值,也减少了它们更新的需要。不但能提高可读性,而且能提升性能(useCallbackuseMemo的更新往往会造成子组件的刷新)。
  2. 示例对比:useState() 和 useReducer()方法

    • 使用 useState()方法计数
     // 使用 useState()方法
     function Counter() {
       const [count, setCount] = useState(0);
       const [step, setStep] = useState(1);
    
       useEffect(() => {
         const id = setInterval(() => {
           setCount(c => c + step); // 依赖其他state来更新
         }, 1000);
         return () => clearInterval(id);
         // 为了保证setCount中的step是最新的,
         // 我们还需要在deps数组中指定step
       }, [step]);
    
       return (
         <>
           <h1>{count}</h1>
           <input value={step} onChange={e => setStep(Number(e.target.value))} />
         </>
       );
     }
    

    讲解: 随着相互依赖的状态变多,setState 的逻辑会变得越来越复杂,useEffect 的 deps 数组也会变得复杂。 降低可读性的同时, useEffect 重新执行的时机变得更加难以预料。

    • 使用 useReducer()方法计数
    function Counter() {
      const initialState = {
        count: 0,
        step: 1,
      };
    
      function reducer(state, action) {
        console.log(state, action)
        const { count, step } = state;
        if (action.type === 'tick') {
          return { count: count + step, step };
        } else if (action.type === 'step') {
          return { count, step: action.step };
        } else {
          throw new Error();
        }
      }
      const [state, dispatch] = useReducer(reducer, initialState);
      const { count, step } = state;
    
      useEffect(() => {
        const id = setInterval(() => {
          dispatch({ type: 'tick' });
          }, 1000);
        return () => clearInterval(id);
      }, []); // deps数组不需要包含step
    
      return (
        <>
          <h1>count:{count}, step: {step}</h1>
          <input value={step} onChange={(e) => dispatch({ type: 'step', step: Number(e.target.value) })} />
        </>
      )
    }
    

    讲解:useReducer()方法使得组件只需要发出action,而无需知道如何更新状态。另外,此时 step 的更新不会造成 useEffect 的失效、重执行。因为此时的 useEffect 依赖于 dispatch,而不是deps中的状态值

内联 reducer 的用法讲解

可以将reducer声明在组件内部,从而能够通过闭包访问props、以及前面的hooks结果:


function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0);
  function reducer(state, action) {
    if (action.type === 'tick') {
      // 可以通过闭包访问到组件内部的任何变量
      // 包括props,以及useReducer之前的hooks的结果
      return state + step;
    } else {
      throw new Error();
    }
  }

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

讲解:

  • 某个button被用户点击,它的onClick被调用,其中执行了dispatch({type:'add'}),React框架安排一次更新
  • React框架处理刚才安排的更新,开始重渲染组件树
  • 渲染到Counter组件的useReducer时,调用reducer(prevState, {type:'add'}) ,处理之前的action 重要的区别在于,reducer是在下次渲染的时候被调用的,它的闭包捕获到了下次渲染的props。