何时使用原生 React.useReducer Hook 以及它与 Redux 的区别

When to use native React.useReducer Hook and how it differentiate from Redux

因此,Hooks 可从 React 16.8 开始使用。从他们的文档来看,Hooks 是功能组件中状态的替代者。基本的钩子是:useStateuseEffectuseContext,但也有一些额外的钩子,其中之一是 useReducer,看起来它使用相同的 action-dispatch 像 Redux 那样的架构。

问题是它是否因为相似而替代 Redux?

它是否更适合特定项目?

它适合放在哪里?

useReducer 的状态对于单个组件是本地的 - 如果您想在整个应用程序中使用此状态,则需要传递它(and/or dispatch功能)通过道具向下。它实际上只是 useState 的更结构化版本 - 事实上,useState is implemented using useReducer under the hood!

另一方面,Redux 做的更多 - 除其他外,它通过上下文使状态对整个应用程序可用,然后提供 API 以在不传递道具的情况下将深层嵌套的组件连接到该状态下来。

换句话说:

  • useReducer 为您提供结构化的 local 状态更新。
  • Redux 为您提供结构化和集中式 状态更新。

如果您想 'roll your own Redux' 使用 Hooks,您需要使用 useReduceruseContext 的某种组合。

Redux 是一个鼓励数据以特定方式流动的库。

react-redux 另一方面实现了 React 友好的方法并提供了很多中间件和包装器,这样库的消费者就不必自己设置整个过程。

虽然 useReducer 是 Redux 工作方式的一部分,但它并不是 Redux 的全部。为了让您在组件的深处使用 dispatch 和 state,您仍然必须组合使用 useContextuseReducer,这就像重新发明轮子一样。

最重要的是 useReducer 只是给了你一个 dispatch 方法,你可以用它来将普通的旧对象作为动作分派。目前还没有办法将 middlewares 添加到 thunksaga 等等。

您也可以使用 useReducer 在您的应用程序中拥有多个 reducer,但是将它们组合成单个存储的方法仍然必须由开发人员管理。

另外 React docs 说明当状态逻辑复杂时 useReduceruseState 的替代方法

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.

useContextuseReducer 等 hook 的作用是消除小型应用程序对 Redux 的依赖。

所以,如果要比较 Redux 和 useReducer

终极版:

  • 中心化状态
  • 打造更多解耦
  • 有中间件:Redux thunk 和 Redux logger
  • 操作只能命中一个商店
  • 可能更适合大项目

使用减速器:

  • 本地状态
  • 没有包装组件
  • 需要 useContext 才能重新发明轮子
  • 附带其他本机挂钩
  • 不需要额外的依赖项
  • 可能有多个存储(实际上是可以充当存储的减速器)
  • 可能更适合小项目

1.) Does useReducer come as a replacement of Redux?

让我们澄清一下 Redux 是什么,这样我们就可以将它的主要部分与带有 useReducer 的普通 React 解决方案进行比较:

(灵感来自 this article

如何用 vanilla React 替换所有部分

动作调度器/状态选择器

这是 React Redux 的替代品:

import { useReducer, useContext, useMemo } from "react"
import { rootReducer } from "./reducers"

const GlobalContext = React.createContext()

const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(rootReducer, { count: 0 });
  const store = useMemo(() => [state, dispatch], [state]);

  // You can also separate dispatch and state context providers
  return (
    <GlobalContext.Provider value={store}>{children}</GlobalContext.Provider>
  );
};

// Provider is placed at top-level to emulate a global state store
ReactDOM.render(<Provider> <App /> </Provider>, document.getElementById('root'))

const Comp = () => {
  // extract this UI logic in its own custom hook for better encapsulation
  const [state, dispatch] = useContext(GlobalContext);
  // ...
};

数据存储

Redux 存储可以实例化 (createStore) 并且访问非常灵活。使用 vanilla React,商店绑定到 UI 中的单个 useReducer。我们可以通过 context 或 props 向下传递它的状态。

存储减速器

与 Redux 一样,完全相同的纯 rootReducer 函数用于 vanilla。

中间件API

redux-thunkredux-saga 是 Redux 中异步操作和副作用最流行的两个 middlewares。使用 useReducer,我们 没有 有任何内置中间件 API。相反,我们先进行异步处理,然后将结果转发给 dispatch:

const [state, dispatch] = useContext(GlobalContext);
// ...
<button onClick={() => dispatchAsync(dispatch)}> Process </button>

const dispatchAsync = dispatch => {
  fetchData().then(data => dispatch({type: "increment"}, data)) 
};

仍然可以将现有的 Redux 中间件与 useReducer 集成,只要它们 common API is covered - you can take a look at 了解更多信息。

Redux DevTools(时间旅行,调试支持)

useReducer 没有 direct 集成可用,因此您可能会在这里错过一个重要的工作流程工具。 reinspect library uses Redux DevTools 也检查 useStateuseReducer(虽然还没有测试)。


2.) Does it suits particular projects better? Where would it fit?

useReducer 用法从本地状态和组件范围开始。正如你所看到的,它也可以提升到全局范围来接管 Redux 的大部分角色。

在某些情况下,useReducer 甚至可以提供更大的灵活性,因为全局状态可以在多个上下文之间进行划分。示例:在不同状态 trees/contexts.

中分离低优先级和高优先级状态更改

使用 vanilla React,您将主要错过 Redux DevTools 和流行的中间件库。另一方面,像 combineReducers can be easily re-implemented for useReducer, as seen .

这样的函数

一般经验法则:开始 在您的应用中使用 vanilla React 并逐步添加全局 Redux 存储等功能,as soon as you feel,这是强制性的(也取决于应用大小)。


const GlobalContext = React.createContext();

const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(rootReducer, { count: 0 });
  const store = useMemo(() => [state, dispatch], [state]);

  // You can also separate dispatch and state context providers
  return (
    <GlobalContext.Provider value={store}>{children}</GlobalContext.Provider>
  );
};

const rootReducer = (state, action) =>
  action === "increment" ? { count: state.count + 1 } : state;

const dispatchAsync = dispatch => {
  // just do the async operation before and invoke dispatch afterwards
  setTimeout(() => dispatch("increment"), 1000);
};

const Comp = () => {
  // You can extract this UI logic in its own custom hook for better encapsulation
  const [state, dispatch] = useContext(GlobalContext);

  return (
    <div>
      <div>Counter: {state.count}</div>
      <button onClick={() => dispatchAsync(dispatch)}>
        Increment async (1sec delay)
      </button>
    </div>
  );
};

ReactDOM.render(<Provider><Comp /></Provider>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useContext, useMemo } = React</script>