与 useState 相比,useReducer 实际上都有哪些优势?

Posted

技术标签:

【中文标题】与 useState 相比,useReducer 实际上都有哪些优势?【英文标题】:What advantages does useReducer actually have over useState?与 useState 相比,useReducer 实际上有哪些优势? 【发布时间】:2021-05-28 21:33:23 【问题描述】:

useState 相比,我很难理解useReducer 何时以及为何具有优势。有很多争论,但对我来说,没有一个是有意义的,在这篇文章中,我试图将它们应用到一个简单的例子中。

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

我们来看这个例子:

版本 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 =&gt; ...)

潜在的其他优势:

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

我看到的缺点:

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

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

【问题讨论】:

【参考方案1】:

我相信这可能会导致意见的争论。然而,从一篇简单的文章中提取的内容对我来说很重要,所以这里在底部有一个指向整篇文章的链接。

useReducer() 是 useState() 的替代方法,它使您可以更好地控制状态管理并简化测试。所有的情况都可以用 useState() 方法来完成,所以总而言之,使用自己熟悉的方法,对你和同事来说更容易理解。

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

【讨论】:

【参考方案2】:

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

Dan Abramovtwitter:

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

React docs

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

React docs:

我们建议在上下文中传递调度,而不是在 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:

并且“dispatch”身份始终是稳定的,即使 reducer 是内联的。因此,您可以依赖它进行性能优化,并将调度作为静态值免费传递给上下文。

实例

在这段代码中,我尝试强调使用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 的身份保持不变!不会触发效果 每次调用它时,我们仍然会得到最新版本的函数!它可以完全访问组件的 props。 不需要记忆/使用回调等。在我看来,仅此一项就可以使代码更简洁,尤其是因为我们应该“依靠 useMemo 作为性能优化,而不是作为语义保证”(react docs)

【讨论】:

以上是关于与 useState 相比,useReducer 实际上都有哪些优势?的主要内容,如果未能解决你的问题,请参考以下文章

[React] When to useReducer instead of useState

useState & useReducer

React 用 useReducer 替换 useState,同时还使用自定义钩子

为啥 setState 回调会抛出错误:“来自 useState() 和 useReducer() Hooks 的状态更新不支持第二个回调参数...”

ZF_react hooks useState的实现 useCallback useMemo useReducer useContext

我收到此错误:“来自 useState() 和 useReducer() Hooks 的状态更新不支持第二个回调...”当我更改状态时