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

Rap*_*ael 5 reactjs react-hooks use-reducer use-state

我正在努力理解useReduceruseState. 有很多争论,但对我来说,没有一个真正有意义,在这篇文章中,我试图将它们应用到一个简单的例子中。

也许我错过了一些东西,但我真的不明白为什么useReducer应该在任何地方使用 over 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>
  );
}
Run Code Online (Sandbox Code Playgroud)

看到这个堆栈闪电战

版本 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>
  );
}
Run Code Online (Sandbox Code Playgroud)

看到这个堆栈闪电战

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

“useReducer 适用于复杂的状态逻辑”。在我们的示例中,假设complexState真的很复杂,我们有许多修改操作,每个操作都有很多逻辑。如何useReducer帮助在这里?对于复杂的状态,拥有单独的功能而不是拥有单个 200 行的减速器功能不是更好吗?

“如果下一个状态取决于前一个状态,则 useReducer 是好的”。我可以用 useState 做同样的事情,不是吗?简单地写setState(oldstate => {...})

潜在的其他优势:

  • “我不必传递多个函数,而只传递一个减速器”:好的,但我也可以使用 useCallback 等将我的函数包装到一个“动作”对象中。正如已经提到的,在不同的函数中有不同的逻辑似乎是对我来说是件好事。
  • “我可以为 reducer 提供一个上下文,这样我的复杂状态就可以在整个应用程序中轻松修改”。是的,但您也可以从该上下文中提供单独的函数(可能由 useCallback 包装)

我看到的缺点:

  • 单个超长函数中的多个不同动作似乎令人困惑
  • 更容易出错,因为您必须检查 reducer 函数或依赖 typescript 等来找出您实际上可以作为操作传递给 reducer 的字符串以及它附带的参数。调用函数时,这要直接得多。

考虑到所有这些:你能给我一个很好的例子,其中useReducer真正闪耀并且不能轻易重写为带有 的版本useState

Rap*_*ael 16

几个月后,我觉得我必须对这个主题添加一些见解。如果在 和 之间进行选择useReducer只是useState个人喜好的问题,为什么人们会写这样的东西:

\n

丹·阿布拉莫夫推特上:

\n
\n

useReducer 确实是 Hooks 的作弊模式。一开始您可能不喜欢它,但它避免了依赖 useState 的类和组件中出现的大量潜在问题。了解使用Reducer。

\n
\n

反应文档

\n
\n

当您具有涉及多个子值的复杂状态逻辑或当下一个状态依赖于前一个状态时,useReducer 通常优于 useState。useReducer 还可以让您优化触发深度更新的组件的性能,因为您可以向下传递调度而不是回调。

\n
\n

反应文档

\n
\n

我们建议在上下文中传递调度,而不是在 props 中传递单独的回调。

\n
\n

因此,让我们尝试将其确定下来并找到一个useReducer清晰可见的场景useState

\n

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

\n

VersionA 的方法(useState&传递回调)可能存在问题:

\n
    \n
  • 出于语义和 linting 的原因,效果应该具有更新功能作为依赖项。
  • \n
  • 然而,这意味着每次重新声明更新函数时都会调用效果。在问题的示例“版本 A”中,这将出现在App!的每次渲染中。
  • \n
  • 调用useCallback函数会有所帮助,但这种模式很快就会变得乏味,特别是当我们需要另外调用useMemo一个actions对象时。(而且我不是这方面的专家,但从性能角度来看,这听起来不太令人信服)
  • \n
  • 此外,如果该函数具有经常更改的依赖项(例如用户在文本字段中的输入),则甚至useCallback不会有太大帮助。
  • \n
\n

如果我们使用减速器:

\n
    \n
  • 减速器的dispatch函数始终具有稳定的身份!(参见反应文档
  • \n
  • 这意味着,我们可以安全地使用它的效果,因为知道它在正常情况下不会改变!即使reducer函数发生变化,dispatch的身份也保持不变并且不会触发效果。
  • \n
  • 然而,当我们调用它时,我们仍然可以获得最新版本的减速函数!
  • \n
\n

再次参见 Dan Abramov 的Twitter 帖子

\n
\n

并且 \xe2\x80\x9cdispatch\xe2\x80\x9d 身份始终稳定,即使减速器是内联的。因此,您可以依靠它进行性能优化,并将调度作为静态值免费传递给上下文。

\n
\n

实际例子

\n

useReducer在此代码中,我尝试强调使用我之前尝试描述的一些优点:

\n
import React, { useEffect } from "react";\nimport { useState, useReducer } from "react";\n\nfunction MyControls({ dispatch }) {\n  // Cool, effect won\'t be called if reducer function changes.\n  // dispatch is stable!\n  // And still the up-to-date reducer will be used if we call it\n  useEffect(() => {\n    function onResize() {\n      dispatch({ type: "set", text: "Resize" });\n    }\n\n    window.addEventListener("resize", onResize);\n    return () => window.removeEventListener("resize", onResize);\n  }, [dispatch]);\n\n  return (\n    <>\n      <button onClick={() => dispatch({ type: "set", text: "ABC" })}>\n        Set to "ABC"\n      </button>\n      <button onClick={() => dispatch({ type: "setToGlobalState" })}>\n        Set to globalAppState\n      </button>\n      <div>Resize to set to "Resized"</div>\n    </>\n  );\n}\n\nfunction MyComponent(props) {\n  const [headlineText, dispatch] = useReducer(reducer, "ABC");\n\n  function reducer(state, action) {\n    switch (action.type) {\n      case "set":\n        return action.text;\n      case "setToGlobalState":\n        // Cool, we can simply access props here. No dependencies\n        // useCallbacks etc.\n        return props.globalAppState;\n      default:\n        throw new Error();\n    }\n  }\n\n  return (\n    <div>\n      <h1>{headlineText}</h1>\n      <MyControls dispatch={dispatch} />\n    </div>\n  );\n}\n\nexport default function App() {\n  const [globalAppState, setGlobalAppState] = useState("");\n\n  return (\n    <div>\n      global app state:{" "}\n      <input\n        value={globalAppState}\n        onChange={(e) => setGlobalAppState(e.target.value)}\n      />\n      <MyComponent globalAppState={globalAppState} />\n    </div>\n  );\n}\n
Run Code Online (Sandbox Code Playgroud)\n

请参阅此代码和框

\n
    \n
  • 尽管reducer函数在每次用户输入时都会发生变化,但dispatch\的身份保持不变!它不会触发效果
  • \n
  • 不过,每次调用该函数时,我们都会获得该函数的最新版本!它可以完全访问组件的 props。
  • \n
  • 无需记忆/useCallback 等。在我看来,仅此一点就使代码更加清晰,特别是因为我们应该“依靠 useMemo 作为性能优化,而不是作为语义保证”(react docs
  • \n
\n