React Hooks 会取代 Redux 吗?

阅读数:3718 2019 年 7 月 29 日 18:05

React Hooks会取代Redux吗?

自从 React 引入了 Hooks API 以来,关于 React Hooks 是否会取代 Redux 的讨论也越来越多了。本文作者认为 React Hooks 不能取代 Redux,并阐述了该观点的理由。

在我看来,Hooks 和 Redux 之间几乎没什么重叠的使用场景。Hooks 没那么神奇,没有什么全新的状态功能;它只是增强了 React 已有 API 本来就能做的那些事情而已。但 hooks API 提升了原生 React 状态 API 的可用性,而且它比被它取代的 class 模型更简单,所以有了 hooks 后我在合适的场景中使用组件状态的频率多了很多。

为了阐明我的观点,首先我们来探讨为什么 Redux 会是首选。

Redux 是什么?

Redux 是一个可预测的状态管理库和架构,可以与 React 轻松集成。

Redux 的主要优点有:

  • 确定性状态视图(与纯组件结合时启用确定性视图呈现)。

  • 事务状态。

  • 状态管理与 I/O 和副作用隔离。

  • 应用程序状态使用单一信息源。

  • 在不同组件之间轻松共享状态。

  • 事务遥测(自动记录操作对象)。

  • 跟踪调试。

总的来说,Redux 提供了强大的代码组织和调试能力。开发者可以使用它构建更易维护的代码,并且在出现故障时更快找到问题的根源。

React Hooks 是什么?

开发者只要用 React Hooks 就能使用状态和 React 生命周期中的众多功能,无需再用 class 和 React 组件生命周期方法。React Hooks 是 React 16.8 中首次引入的。

React Hooks 的主要优点有:

  • 无需 class 也能在组件生命周期中使用状态和 Hooks。

  • 将相关逻辑集中起来放进组件,而不是把它们分散到各个生命周期方法中。

  • 与组件实现(例如呈现 prop 模式)独立的共享可复用行为。

请注意,React Hooks 的这些吸引人的优点和 Redux 并没有重叠。你应该使用 React Hooks 来获得确定的状态更新,但这也是 React 本身一直就有的功能,而 Redux 的确定性状态模型与它配合的很好。这就是 React 提供确定性视图呈现的手段,而且也是促使 React 诞生的动力之一。

现在有了 react-redux hooks API 和 React 的 useReducer hooks 等工具,你用不着在 React hooks 和 Redux 之间二选一了。鱼与熊掌可以兼得,你应该两个都用,搭配起来干活儿。

Hooks 取代了哪些技术?

自从 Hooks API 诞生之后,我不再使用的技术有:

  • class 组件。
  • 呈现 prop 模式。

Hooks 不能取代的技术有哪些?

我还在经常使用的技术有:

  • Redux,原因如前所述。

  • 高阶组件(Higher Order Components),这些组件负责构成由全部或多数应用程序视图共享的交叉关注点(cross-cutting concerns,也称横切关注点),例如 Redux 提供程序、公共布局提供程序、配置提供程序、身份验证 / 授权程序、i18n 等。

  • 容器和显示组件之间的隔离,以实现更好的模块化,改善可测试性,更容易分离视觉效果和纯逻辑。

何时使用 Hooks

不是说所有应用或组件都得用 Redux 的。如果你的应用只有单个视图,不保存或加载状态,并且没有异步 I/O,那么引入 Redux 只会让应用变复杂而已。

同样,如果你的组件:

  • 不使用网络。

  • 不保存或加载状态。

  • 不与其他非子组件共享状态。

  • 需要一些临时的本地组件状态。

那么你就很适合使用 React 的内置组件状态模型。在这些情况下 React hooks 就能大显身手了。例如,下面这个表单使用了 React 中的本地组件状态 useState hook。

复制代码
import React, { useState } from 'react';
import t from 'prop-types';
import TextField, { Input } from '@material/react-text-field';
const noop = () => {};
const Holder = ({
itemPrice = 175,
name = '',
email = '',
id = '',
removeHolder = noop,
showRemoveButton = false,
}) => {
const [nameInput, setName] = useState(name);
const [emailInput, setEmail] = useState(email);
const setter = set => e => {
const { target } = e;
const { value } = target;
set(value);
};
return (
<div className="row">
<div className="holder">
<div className="holder-name">
<TextField label="Name">
<Input value={nameInput} onChange={setter(setName)} required />
</TextField>
</div>
<div className="holder-email">
<TextField label="Email">
<Input
value={emailInput}
onChange={setter(setEmail)}
type="email"
required
/>
</TextField>
</div>
{showRemoveButton && (
<button
className="remove-holder"
aria-label="Remove membership"
onClick={e => {
e.preventDefault();
removeHolder(id);
}}
>
&times;
</button>
)}
</div>
<div className="line-item-price">${itemPrice}</div>
<style jsx>{cssHere}</style>
</div>
);
};
Holder.propTypes = {
name: t.string,
setName: t.func,
email: t.string,
setEmail: t.func,
itemPrice: t.number,
id: t.string,
removeHolder: t.func,
showRemoveButton: t.bool,
};
export default Holder;

这里的代码使用 useState 来跟踪要填入名称和电子邮件的临时表单的输入状态:

复制代码
const [nameInput, setName] = useState(name);
const [emailInput, setEmail] = useState(email);

你可能会注意到还有一个 removeHolder 动作创建者,加入来自 Redux 的 prop。各种方法可以混合搭配。

解决这类问题时都可以使用本地组件状态,但我首先想到的办法可能就是将它塞到 R​​edux 里面,并把状态从 prop 中拿出来吧。

使用组件状态意味着要使用 class 组件,还要使用 class 实例属性语法(或 constructor 函数)设置初始状态,等等——就为了不用 Redux,麻烦事也太多了。Redux 有一些管理表单状态的即插即用工具,因此我不必担心临时表单状态渗入我的业务逻辑。

我新开发的应用几乎都用到了 Redux,以前做选择是很简单的:几乎所有项目都用 Redux 就行了!

现在这个选择还是很简单:

组件状态用组件状态,应用状态用 Redux。

何时使用 Redux

另一个常见问题是“你应该把所有东西放在 Redux 中吗?不这样做的话,它会不会破坏跟踪调试呢?“

答案是不会的,因为应用程序中有很多状态是临时的,而且过于粗糙,无法为日志遥测或跟踪调试提供比较有用的信息。除非你正在构建一个实时协作的编辑器,否则你可能并不需要将所有用户按键或鼠标移动行为放进 Redux 状态里。当你向 Redux 状态添加内容时,加进来的是一个抽象层以及随之而来的复杂度。

换句话说,你大可放心使用 Redux,但是每次用到它时应该有合适的理由。

如果你的组件有以下需求,那么 Redux 会很有用:

  • 使用网络之类的 I/O 或设备 API。

  • 保存或加载状态。

  • 与非子组件共享其状态。

  • 需要与应用程序的其他部分共享业务逻辑或数据处理过程

下面这个例子来自 TDDDay 应用

复制代码
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { compose } from 'ramda';
import page from '../../hocs/page.js';
import Purchase from './purchase-component.js';
import { addHolder, removeHolder, getHolders } from './purchase-reducer.js';
const PurchasePage = () => {
// You can use these instead of
// mapStateToProps and mapDispatchToProps
const dispatch = useDispatch();
const holders = useSelector(getHolders);
const props = {
// Use function composition to compose action creators
// with dispatch. See "Composing Software" for details.
addHolder: compose(
dispatch,
addHolder
),
removeHolder: compose(
dispatch,
removeHolder
),
holders,
};
return <Purchase {...props} />;
};
// `page` is a Higher Order Component composed of many
// other higher order components using function composition.
export default page(PurchasePage);

这个组件并不处理 DOM。这是一个展示组件。它使用 React-Redux hooks API 连接到 Redux 上。

之所以它会用到 Redux 是因为 UI 的其他部分需要这个表单的数据,另外当购买流程完成后,我们需要将数据保存到数据库中。

它的状态不是本地化到单个组件中,而是在组件之间共享的;状态是持久而非临时的,并且它可能跨越多个页面视图或会话。除非你在 React API 之上构建自己的状态容器库,否则这些都是本地组件状态无法实现的效果——但状态容器库比 Redux 要复杂得多。

将来,React 的 suspense API 或许能用来保存和加载状态。等到 suspense API 发布后,我们就能知道它能否取代 Redux 中的保存 / 加载模式了。Redux 允许我们干净地将副作用与其他组件逻辑分离开来,不需要我们模拟 I/O 服务。(相比 thunk,我更喜欢使用 redux-saga 的原因就是后者的隔离功能)。为了在这个用例上追赶 Redux 的脚步,React 的 API 需要提供副作用隔离功能。

Redux 是一种架构

Redux 与状态管理库有着很大区别。它本质上也是 Flux 架构的一个子集,更关注状态变化的过程。我在另一篇博文中详细介绍了 Redux 架构

当我需要复杂的组件状态但又用不着 Redux 库时,我经常会使用 redux 风格的 Reducer。我还会使用 Redux 风格的操作(甚至是 Redux 工具,如 Autodux 和 redux-saga 等)调度 Node 应用中的操作,这样就无需导入 Redux 库了。

与库相比,Redux 向来更接近一种架构和非强制性的约定(convention)。事实上,Redux 的基本实现只需要几十行代码就能复现。

这也是 Redux 的一大好处。如果你想多用一些本地组件状态和 hook API,不想把所有内容都塞到 Redux 里,那也完全没问题。

React 提供了一个 useReducer hool,可以用它接入你的 Redux 风格的 Reducer。这对不常见的状态逻辑、依赖状态等内容非常有用。如果你的用例是要将临时状态装入单个组件,则可以使用 Redux 架构,但要用 useReducer hook 取代 Redux 来管理状态。

如果你随后需要维持或分享这个状态,那么大部分工作其实已经做完了。剩下的就是连接组件并将 Reducer 添加到 Redux 存储了。

答疑

“如果不把所有内容都装进 Redux,会损害确定性吗?”

不会的。实际上 Redux 也没有强制执行确定性机制。能这样做的是约定。如果你希望 Redux 状态是确定性的,请使用纯函数。如果你希望临时组件状态是确定性的,也要用纯函数。

“难道你不需要 Redux 作为单一信息源吗?”

单一信息源原则并不是说你要让所有状态有同一个来源。相反,它意味着对于每一个状态来说,该状态应该有一个单一信息源。你可以有许多不同的状态,每个状态都有自己的单一信息源。

这意味着你可以选择让哪些内容进入 Redux 的内容或组件状态。你还可以从其他来源获取状态,例如从浏览器 API 获取当前位置 href。

Redux 作为应用程序状态的单一信息源是很好用的,但如果组件状态本地化为单个组件,并且仅在一处使用,那么根据定义它已经为该状态提供了单一信息源:也就是 React 组件状态。

如果你将某些内容放入 Redux 状态,则应始终从 Redux 状态读取它们。对于 Redux 中的所有状态来说,Redux 应该是它们的单一信息源。

需要的话可以将所有内容都放在 Redux 中。这可能会对需要频繁更新的状态或具有大量依赖状态的组件产生性能影响。一般来说你用不着考虑性能瓶颈,但真遇到性能问题时就分别尝试一下调节参数和 RAIL 性能模型两种方法,看看有没有效果。

“我应该使用 react-redux 连接还是 hooks API?”

具体情况具体分析。connect 创建一个可复用的高阶组件,而 hooks API 则是针对单个组件的集成优化的。你是要将同样的存储 prop 连接到其他组件吗?那就用 connect。否则我更喜欢 hooks API 的读取方式。例如,假设你有一个处理用户操作权限授权的组件:

复制代码
import { connect } from 'react-redux';
import RequiresPermission from './requires-permission-component';
import { userHasPermission } from '../../features/user-profile/user-profile-reducer';
import curry from 'lodash/fp/curry';
const requiresPermission = curry(
(NotPermittedComponent, { permission }, PermittedComponent) => {
const mapStateToProps = state => ({
NotPermittedComponent,
PermittedComponent,
isPermitted: userHasPermission(state, permission),
});
return connect(mapStateToProps)(RequiresPermission);
},
);
export default requiresPermission;

现在,如果你有一堆管理员视图都需要管理员权限,那么你可以创建一个高阶组件,根据这个权限需求为它们构造所需的交叉关注点:

复制代码
import NextError from 'next/error';
import compose from 'lodash/fp/compose';
import React from 'react';
import requiresPermission from '../requires-permission';
import withFeatures from '../with-features';
import withAuth from '../with-auth';
import withEnv from '../with-env';
import withLoader from '../with-loader';
import withLayout from '../with-layout';
export default compose(
withEnv,
withAuth,
withLoader,
withLayout(),
withFeatures,
requiresPermission(() => <NextError statusCode={404} />, {
permission: 'admin',
}),
);

要使用它时:

复制代码
import compose from 'lodash/fp/compose';
import adminPage from '../HOCs/admin-page';
import AdminIndex from '../features/admin-index/admin-index-component.js';
export default adminPage(AdminIndex);

高阶组件 API 对于这类用例来说是很方便的,它实际上比 hooks API 更简洁(需要的代码更少),但是为了读取 connect API,你必须记住它需要 mapStateToProps 作为第一个参数,用 mapDispatchToProps 作为第二个参数;你大概知道它可以读取函数或对象字面量,也知道这些行为的差异。你还需要记住它是 curry 过的,但不是自动 curry 的。

换句话说,connect API 的代码更简洁,但它不是特别易读,不那么友好。如果我不需要为其他组件复用连接,我更喜欢可读性好得多的 hooks API,多打一些代码也无妨。

“如果单例模式是反模式,而 Redux 是单例模式,那么 Redux 不就是反模式吗?”

并不是。单例模式是一种代码形式,可以表明共享的可变状态,这是真正的反模式。Redux 通过封装防止了共享可变状态(你不应该直接在 reducer 之外改变应用状态,而要让 Redux 处理状态更新)和消息传递(只有调度的动作对象才能触发状态更新)。

更多内容

可在 EricElliottJS.com 上了解有关 React 和 Redux 的更多信息。网站上有很多示例和视频,深入讨论本文涉及到的代码示例、函数组合等内容。

在这里了解如何对 React 组件进行单元测试: https://medium.com/javascript-scene/unit-testing-react-components-aeda9a44aae2?source=post_page;

至于测试这方面的内容,可以在 TDDDay.com 上了解测试驱动开发(TDD)。

原文链接 https://medium.com/javascript-scene/do-react-hooks-replace-redux-210bab340672

评论

发布
用户头像
https://stackblitz.com/edit/hook-setup?file=index.js
hook, renderProps, class 3种写法高度统一,任君切换
setup, computed, watch, sync 等特性,赋能react更多玩法
powered by concent
2019 年 08 月 15 日 10:20
回复
用户头像
但我还是要说, 不要为了使用 Redux 而使用 Redux.
2019 年 07 月 30 日 15:45
回复
没有更多了