【React的做弊模式】理解useReducer的优点和高级用法

或许你已经知道,“当多个state须要一块儿更新时,就应该考虑使用useReducer”;或许你也已经据说过,“使用useReducer可以提升应用的性能”。可是篇文章但愿帮助你理解:为何useReducer能提升代码的可读性和性能,以及如何在reducer中读取props的值。react

因为useReducer造就的解耦模式以及高级用法,React团队的Dan Abramov将useReducer描述为"React的做弊模式"数组

useReducer的优点

举一个例子:闭包

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替代useState之后:ide

function Counter() {
  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}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  )
}

如今组件只须要发出action,而无需知道如何更新状态。也就是将What to doHow to do解耦。完全解耦的标志就是:useReducer老是返回相同的dispatch函数(发出action的渠道),无论reducer(状态更新的逻辑)如何变化。函数

这是useReducer的逆天之处之一,下面会详述

另外一方面,step的更新不会形成useEffect的失效、重执行。由于如今useEffect依赖于dispatch,而不依赖于状态值(得益于上面的解耦模式)。这是一个重要的模式,能用来避免useEffect、useMemo、useCallback须要频繁重执行的问题性能

如下是state的定义,其中reducer封装了“如何更新状态”的逻辑:ui

const initialState = {
  count: 0,
  step: 1,
};

function reducer(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();
  }
}

总结:es5

  • 当状态更新逻辑比较复杂的时候,就应该考虑使用useReducer。由于:spa

    • reducer比setState更加擅长描述“如何更新状态”。好比,reducer可以读取相关的状态、同时更新多个状态。
    • 【组件负责发出action,reducer负责更新状态】的解耦模式,使得代码逻辑变得更加清晰。
    • 简单来记,就是每当编写setState(prevState => newState)的时候,就应该考虑是否值得将它换成useReducer。
  • 经过传递useReducer的dispatch,能够减小状态值的传递

    • useReducer老是返回相同的dispatch函数,这是完全解耦的标志:状态更新逻辑能够任意变化,而发起actions的渠道始终不变
    • 得益于前面的解耦模式,useEffect函数体、callback function只须要使用dispatch来发出action,而无需直接依赖状态值。所以在useEffect、useCallback、useMemo的deps数组中无需包含状态值,也减小了它们更新的须要。不但能提升可读性,并且能提高性能(useCallback、useMemo的更新每每会形成子组件的刷新)。

高级用法:内联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>;
}

这个能力可能会出乎不少人的意料。由于大部分人对reducer的触发时机的理解是错误的(包括之前的我)。我之前理解的触发时机是这样:

  1. 某个button被用户点击,它的onClick被调用,其中执行了dispatch({type:'add'}),React框架安排一次更新
  2. React框架处理刚才安排的更新,调用reducer(prevState, {type:'add'}),来获得新的状态
  3. React框架用新的状态来渲染组件树,渲染到Counter组件的useReducer时,返回上一步获得的新状态便可

可是实际上,React会在下次渲染的时候再调用reducer来处理action:

  1. 某个button被用户点击,它的onClick被调用,其中执行了dispatch({type:'add'}),React框架安排一次更新
  2. React框架处理刚才安排的更新,开始重渲染组件树
  3. 渲染到Counter组件的useReducer时,调用reducer(prevState, {type:'add'}) ,处理以前的action

重要的区别在于,被调用的reducer是本次渲染的reducer函数,它的闭包捕获到了本次渲染的props。

若是按照上面的错误理解,被调用的reducer是上次渲染的reducer函数,它的闭包捕获到上次渲染的props(由于本次渲染还没开始呢)

事实上,若是你简单地使用console.log来打印执行顺序,会发现reducer是在新渲染执行useReducer的时候被同步执行的

console.log("before useReducer");
  const [state, dispatch] = useReducer(reducer, initialState);
  console.log("after useReducer", state);

  function reducer(prevState, action) {
    // these current state var are not initialized yet
    // would trigger error if not transpiled to es5 var
    console.log("reducer run", state, count, step);
    return prevState;
  }

调用dispatch之后会输出:

before useReducer
reducer undefined undefined undefined
after useReducer {count: 1, step: 1}

证实reducer确实被useReducer同步地调用来获取新的state。
codesandbox demo

参考资料

相关文章
相关标签/搜索