useReducer 实际上比 useState 有什么优势?

What advantages does useReducer actually have over useState?

我很难理解与 useState 相比,useReducer 究竟何时以及为何具有优势。那里有很多论点,但对我来说,其中 none 是有道理的,在这个 post 中,我试图将它们应用到一个简单的例子中。

也许我遗漏了一些东西,但我不明白为什么 useReducer 应该在 useState 之上的任何地方使用。我希望你能帮我澄清一下。

举个例子:

版本 A - 带有 useState

function CounterControls(props) {
  return (
    <>
      <button onClick={props.increment}>increment</button>
      <button onClick={props.decrement}>decrement</button>
    </>
  );
}

export default function App() {
  const [complexState, setComplexState] = useState({ nested: { deeply: 1 } });

  function increment() {
    setComplexState(state => {
      // do very complex logic here that depends on previous complexState
      state.nested.deeply += 1;
      return { ...state };
    });
  }

  function decrement() {
    setComplexState(state => {
      // do very complex logic here that depends on previous complexState
      state.nested.deeply -= 1;
      return { ...state };
    });
  }

  return (
    <div>
      <h1>{complexState.nested.deeply}</h1>
      <CounterControls increment={increment} decrement={decrement} />
    </div>
  );
}

看到这个stackblitz

版本 B - 使用 useReducer

import React from "react";
import { useReducer } from "react";

function CounterControls(props) {
  return (
    <>
      <button onClick={() => props.dispatch({ type: "increment" })}>
        increment
      </button>
      <button onClick={() => props.dispatch({ type: "decrement" })}>
        decrement
      </button>
    </>
  );
}

export default function App() {
  const [complexState, dispatch] = useReducer(reducer, {
    nested: { deeply: 1 }
  });

  function reducer(state, action) {
    switch (action.type) {
      case "increment":
        state.nested.deeply += 1;
        return { ...state };
      case "decrement":
        state.nested.deeply -= 1;
        return { ...state };
      default:
        throw new Error();
    }
  }

  return (
    <div>
      <h1>{complexState.nested.deeply}</h1>
      <CounterControls dispatch={dispatch} />
    </div>
  );
}

看到这个stackblitz

在很多文章中(包括docs)有两个论点似乎很流行:

“useReducer 适用于复杂的状态逻辑”。 在我们的示例中,假设 complexState 很复杂,有很多修改操作,每个修改操作都有很多逻辑。 useReducer 在这里有什么帮助?对于复杂的状态,拥有单独的函数而不是拥有一个 200 行的 reducer 函数不是更好吗?

“如果下一个状态依赖于前一个状态,useReducer 很好”。 我可以用 useState 做完全相同的事情,不是吗?只需写 setState(oldstate => {...})

其他潜在优势:

我看到的缺点:

考虑到所有这些:你能给我一个很好的例子,其中 useReducer 确实很出色,并且不能轻易地重写为 useState 的版本吗?

我相信这可能会以意见争论告终。然而,这篇从一篇简单文章中摘录的文章对我来说很重要,所以这里是整篇文章底部的 link。

useReducer() is an alternative to useState() which gives you more control over the state management and can make testing easier. All the cases can be done with useState() method, so in conclusion, use the method that you are comfortable with, and it is easier to understand for you and colleagues.

参考。文章:https://dev.to/spukas/3-reasons-to-usereducer-over-usestate-43ad#:~:text=useReducer()%20is%20an%20alternative,understand%20for%20you%20and%20colleagues.

几个月后,我觉得我必须对这个话题补充一些见解。如果在 useReduceruseState 之间选择只是个人喜好问题,为什么人们会写这样的东西:

Dan Abramov on twitter:

useReducer is truly the cheat mode of Hooks. You might not appreciate it at first but it avoids a whole lot of potential issues that pop up both in classes and in components relying on useState. Get to know useReducer.

React docs

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.

React docs:

We recommend to pass dispatch down in context rather than individual callbacks in props.

所以让我们尝试确定并找到一个场景,其中 useReducer 明显优于 useState:

如果需要从嵌套组件中的 `useEffect` 调用更新函数怎么办?

VersionA 的方法(useState & 向下传递回调)可能有问题:

  • 出于语义和 linting 的原因,效果应该将更新函数作为依赖项。
  • 然而,这意味着每次重新声明更新函数时都会调用效果。在问题的示例“版本 A”中,这将出现在 App!
  • 的每个渲染器中
  • 在函数上调用 useCallback 会有所帮助,但这种模式很快就会变得乏味,尤其是当我们需要在 actions 对象上另外调用 useMemo 时。 (我也不是这方面的专家,但从性能的角度来看,这听起来不太有说服力)
  • 此外,如果函数具有经常变化的依赖项(如用户输入),即使 useCallback 也无济于事。

如果我们改用减速器:

  • reducer的dispatch函数始终有一个稳定的标识! (参见 react docs
  • 这意味着我们可以放心地在效果器中使用它,因为它在正常情况下不会发生变化!即使 reducer-function 发生变化,dispatch 的身份保持不变并且不会触发效果。
  • 然而,当我们调用它时,我们仍然会得到最新版本的 reducer-function!

再次请参阅 Dan Abramov 的 Twitter Post:

And the “dispatch” identity is always stable, even if the reducer is inline. So you can rely on it for perf optimizations and pass dispatch down the context for free as a static value.

实例

在这段代码中,我试图强调使用 useReducer 的一些优势,我之前曾尝试描述过:

import React, { useEffect } from "react";
import { useState, useReducer } from "react";

function MyControls({ dispatch }) {
  // Cool, effect won't be called if reducer function changes.
  // dispatch is stable!
  // And still the up-to-date reducer will be used if we call it
  useEffect(() => {
    function onResize() {
      dispatch({ type: "set", text: "Resize" });
    }

    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, [dispatch]);

  return (
    <>
      <button onClick={() => dispatch({ type: "set", text: "ABC" })}>
        Set to "ABC"
      </button>
      <button onClick={() => dispatch({ type: "setToGlobalState" })}>
        Set to globalAppState
      </button>
      <div>Resize to set to "Resized"</div>
    </>
  );
}

function MyComponent(props) {
  const [headlineText, dispatch] = useReducer(reducer, "ABC");

  function reducer(state, action) {
    switch (action.type) {
      case "set":
        return action.text;
      case "setToGlobalState":
        // Cool, we can simply access props here. No dependencies
        // useCallbacks etc.
        return props.globalAppState;
      default:
        throw new Error();
    }
  }

  return (
    <div>
      <h1>{headlineText}</h1>
      <MyControls dispatch={dispatch} />
    </div>
  );
}

export default function App() {
  const [globalAppState, setGlobalAppState] = useState("");

  return (
    <div>
      global app state:{" "}
      <input
        value={globalAppState}
        onChange={(e) => setGlobalAppState(e.target.value)}
      />
      <MyComponent globalAppState={globalAppState} />
    </div>
  );
}

看到这个codesandbox

  • 即使 reducer 函数在每个用户输入时都会发生变化,dispatch 的身份保持不变!不触发效果
  • 我们每次调用函数时仍然会得到最新版本的函数!它可以完全访问组件的道具。
  • 不需要memoizing/useCallback等。在我看来,仅此一项就使代码更清晰,特别是因为我们应该“依赖 useMemo 作为性能优化,而不是语义保证”(react docs)