useState 与 useReducer

useState vs useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

(引自https://reactjs.org/docs/hooks-reference.html#usereducer

我对粗体部分感兴趣,它指出在上下文中使用时应使用 useReducer 而不是 useState

我尝试了两种变体,但它们似乎没有什么不同。

我比较这两种方法的方式如下:

const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);

我将它们中的每一个都传递给一个上下文 object,它在更深层次的 child 中被使用(我只是 运行 单独测试,用函数替换值我想测试)。

<ContextObject.Provider value={updateState // dispatch}>

child 包含这些函数

const updateFunction = useContext(ContextObject);
useEffect(
  () => {
    console.log('effect triggered');
    console.log(updateFunction);
  },
  [updateFunction]
);

在这两种情况下,当 parent 重新渲染时(由于另一个本地状态更改),效果永远不会 运行,表明更新函数在渲染之间没有更改。 我读错了引文中的粗体句子吗?还是我忽略了什么?

useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

以上声明并不是要表明 useState 返回的 setter 是在每次更新或渲染时新创建的。这意味着当你有一个复杂的逻辑来更新状态时,你不会直接使用 setter 来更新状态,而是编写一个复杂的函数,该函数反过来会调用 setter更新后的状态类似于

const handleStateChange = () => {
   // lots of logic to derive updated state
   updateState(newState);
}

ContextObject.Provider value={{state, handleStateChange}}>

在上面的例子中,每次重新渲染父级时,都会创建一个新的 handleStateChange 实例,从而导致上下文消费者也重新渲染。

上述情况的解决方案是使用useCallback并记住状态更新器方法并使用它。但是为此,您需要处理与在方法中使用值相关的关闭问题。

因此建议使用 useReducer 其中 returns 一个 dispatch 方法,它在重新渲染之间不会改变,并且您可以在 reducer 中拥有操作逻辑。

需要关心的时候

如果您在 render 上创建回调并将其传递给 child 组件,该 child 的 props 将会改变。但是,当 parent 渲染时,常规组件将重新渲染(到虚拟 dom),甚至道具保持不变。例外是 class 实现 shouldComponentUpdate 并比较 props 的组件(例如 PureComponent)。

这是一个优化,只有在重新渲染 child 组件需要大量计算时才应该关心它(如果将它多次渲染到同一屏幕,或者它需要深度或显着重新渲染)。

如果是这种情况,您应该确保:

  1. 您的 child 是一个扩展 PureComponent
  2. 的 class 组件
  3. 避免将新创建的函数作为 prop 传递。相反,通过 dispatch,从 React.useState 返回的 setter 或者 memoized 自定义 setter.

使用 memoized 自定义 setter

虽然我不建议为特定组件构建一个独特的记忆 setter(您需要注意一些事项),但您可以使用一个通用的挂钩来为您实现.

这是一个 useObjState 挂钩的示例,它提供了一个简单的 API,并且不会导致额外的重新呈现。


const useObjState = initialObj => {
  const [obj, setObj] = React.useState(initialObj);
  const memoizedSetObj = React.useMemo(() => {
    const helper = {};
    Object.keys(initialObj).forEach(key => {
      helper[key] = newVal =>
        setObj(prevObj => ({ ...prevObj, [key]: newVal }));
    });
    return helper;
  }, []);
  return [obj, memoizedSetObj];
};

function App() {
  const [user, memoizedSetUser] = useObjState({
    id: 1,
    name: "ed",
    age: null,
  });

  return (
      <NameComp
        setter={memoizedSetUser.name}
        name={user.name}
      />
  );
}

const NameComp = ({name, setter}) => (
  <div>
    <h1>{name}</h1>
      <input
        value={name}
        onChange={e => setter(e.target.value)}
      />
  </div>
)

Demo