自从钩子出现在React以来,关于它们是否可以代替Redux一直存在很多问题。
我相信钩子和Redux没有什么共同点。 钩子并没有给我们提供与国家合作的新的令人惊奇的机会。 相反,它们扩展了API,以便它们可以在React中完成已有的工作。 但是,钩子API使使用React的状态管理功能更加方便。 事实证明,使用新功能处理状态要比基于类的组件中可用的旧功能容易。 现在,我比以前更多地使用这些工具来处理组件的状态。 自然,我只会在适当的时候这样做。

为了解释我对React钩子和Redux的态度,我想首先谈谈通常使用Redux的情况。
什么是Redux?
Redux是一个实现可预测的应用程序状态存储的库。 它也是与React无缝集成的架构。
以下是Redux的主要优势:
- 状态的确定性表示(与纯组件结合使用,可以形成确定性的视觉元素)。
- 支持事务状态更改。
- 从I / O机制和副作用中隔离状态管理。
- 该状态存在单个可靠数据源。
- 轻松组织与国家在各个组件中的协作。
- 事务分析工具(自动记录操作对象)。
- 具有记录和播放程序执行过程的能力(时间旅行调试,TTD)。
换句话说,Redux使您可以很好地组织代码并方便地对其进行调试。 Redux帮助开发易于维护的应用程序。 使用此库可以更轻松地查找程序中出现的问题的根源。
什么是React钩子?
当使用功能组件时,React钩子允许根据类的生命周期方法及其类使用组件状态的类似物。 挂钩出现在React 16.8中。
钩子的主要优点如下:
- 无需使用基于类的组件即可使用状态和处理组件生命周期事件的能力。
- 将相关逻辑联合存储在相同的组件位置,而不是在几种生命周期方法之间拆分相似的逻辑。
- 独立于组件实现的共享机制(这类似于渲染道具模板)。
请注意,这些出色的功能实际上并不会淹没Redux。 React挂钩可以并且应该用于执行确定性状态更新,但这一直是React功能之一,并且Redux确定性状态模型与该功能很好地结合在一起。 这就是React在视觉元素输出中实现确定性的方式,并且毫不夸张地说,这是创建React的驱动动机之一。
如果您使用
带有hooks的react-redux API或
React useReducer hook之类的工具 ,您会发现没有理由问选择什么
-hooks或Redux。 您可以同时使用和组合这些技术。
用什么代替钩子?
挂钩API出现之后,我停止使用以下技术:
什么不代替钩子?
我仍然经常使用以下技术:
- Redux-出于上述所有原因。
- 高阶组件-为了在我必须实现应用程序的全部或某些可视组件共享的端到端功能的情况下执行组件组合的目的。 此类功能包括Redux提供程序,页面布局系统,应用程序设置支持系统,身份验证和授权工具,应用程序国际化工具等。
- 容器组件和具有视觉表示的组件之间的分隔。 这样可以提高应用程序的模块化和可测试性,最好将效果和纯逻辑分开。
何时使用挂钩?
无需努力在每个应用程序和每个组件中使用Redux。 如果您的项目由一个可视组件组成,如果它不保存数据也不从那里加载数据,并且如果其中不执行异步I / O操作,那么我就无法通过在其中使用Redux来使该项目复杂化。
具有以下功能的组件也可以这样说:
- 他们不使用网络资源。
- 它们不会以状态存储数据,也不会从那里加载数据。
- 它们不与不是其子孙的其他组件共享状态。
- 它们没有自己的特定状态,用于短期数据存储。
在某些情况下,您可能有充分的理由使用标准的React组件状态模型。 在这种情况下,React钩子将为您带来出色的工作。 例如,下面描述的表单通过React
useState
挂钩使用组件的本地状态。
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); }} > × </button> )} </div> <div className="line-item-price">${itemPrice}</div> <style jsx>{cssHere}</style> </div> ); }; Holder.propTypes = { name: t.string, email: t.string, itemPrice: t.number, id: t.string, removeHolder: t.func, showRemoveButton: t.bool, }; export default Holder;
这里
useState
用于控制
name
和
email
输入字段的简要使用状态:
const [nameInput, setName] = useState(name); const [emailInput, setEmail] = useState(email);
您可能会注意到Redux的属性中仍然有
removeHolder
动作的创建者。 如前所述,技术的结合和结合是完全正常的。
使用组件的本地状态来解决此类问题一直看起来很好,但是在任何情况下,在使用React钩子之前,我都想将组件数据保存在Redux存储中并从属性中获取状态。
以前,处理组件的状态涉及使用基于类的组件,使用声明类属性的机制(或在类构造函数中)将初始数据写入状态。 结果,为了避免使用Redux,该组件必须太复杂。 Redux还赞成使用方便的工具来使用Redux管理表单状态。 结果,以前,我不必担心表单的临时状态与寿命更长的数据存储在同一位置。
由于我已经在所有或多或少复杂的应用程序中使用过Redux,因此选择用于存储组件组件状态的技术并没有引起我太多思考。 我几乎在所有情况下都使用过Redux。
在现代条件下,做出选择也很容易:使用标准的React机制来组织处理组件的状态,并使用Redux管理应用程序的状态。
什么时候使用Redux?
关于状态管理的另一个常见问题是:“我是否需要将所有内容绝对放到Redux存储库中? 如果我不这样做,是否会违反使用TTD机制调试应用程序的能力?”
完全不需要在Redux存储库中托管所有内容。 事实是,应用程序使用了很多临时数据,这些临时数据散布在其周围,无法提供一些信息,这些信息记录在日志中或在调试期间使用,可以为开发人员发现问题提供重要帮助。 可能是您,除非您正在编写实时编辑器应用程序,否则不需要每次鼠标移动或每次击键都向状态写入。 当您将某些东西置于Redux状态时,您将向应用程序添加一个附加级别的抽象,以及随之而来的附加复杂性。
换句话说,您可以安全地使用Redux,但是必须有某些原因。 如果组件在以下功能方面有所不同,则可以在组件中使用Redux功能:
- 他们使用I / O。 例如,它们与网络或某些设备一起使用。
- 他们将数据保存到其中或从中加载数据。
- 他们将状态与不是其后代的组件一起使用。
- 他们处理应用程序其他部分处理的任何业务逻辑,或者处理应用程序其他部分中使用的数据。
这是取自
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 = () => {
本文档不涉及DOM。 这是一个演示组件。 它使用
带有hook支持的react-redux API连接到Redux。
这里使用Redux是因为我们需要此表单处理的数据以在用户界面的其他部分中使用。 购买操作完成后,我们需要将相关信息保存在数据库中。
此代码使用的状态片段由各种组件使用;它们不仅由一个组件处理。 这不是数据,仅存在很短的时间。 该数据可以被认为是永久性的,可以在应用程序的不同屏幕上以及多个会话中使用。 所有这些都是无法应用用于存储数据的组件状态的情况。 没错,这仍然可行,但前提是应用程序的创建者必须基于React API编写自己的状态管理库。 这比仅使用Redux困难得多。
将来,当以状态存储数据并从中加载数据时,React
Suspense API可能会派上用场。 我们需要等待它的发布,看看它是否可以替换用于保存和加载Redux数据的模板。 Redux允许我们将副作用与其余组件逻辑清楚地分开,而我们不需要以特殊方式使用I / O服务。 (相对于
redux-thunk中间件 ,我更喜欢
redux-saga库的原因是效果隔离)。 为了在这种情况下与Redux竞争,React API将需要提供效果隔离。
Redux是架构
Redux比状态管理库多得多(并且常常少得多)。 它也是
Flux体系结构的子集,它更严格地定义了状态更改的实现方式。
在此处阅读有关Redux架构的更多信息。
当我需要维护组件的复杂状态,而无需使用Redux库时,我经常使用以Redux样式创建的reducer。 我还使用了根据Redux精神创建的动作(甚至包括Redux工具,如
Autodux和
redux-saga )将动作发送到Node.js应用程序。 但是,我什至没有将Redux导入此类应用程序。
Redux项目一直比图书馆更像是一个体系结构和一组自愿协议。 实际上,Redux的基本实现实际上可以用几十行代码来布局。
对于那些想更频繁地使用带有钩子的组件的本地状态并且不将所有内容绑定到Redux的人来说,这将是个好消息。
React支持
useReducer
钩子,该钩子可以与Redux风格的reducer一起使用。 这对于实现处理状态的非平凡逻辑,处理状态的依存碎片等很有用。 如果遇到单个组件的临时状态适合的问题,则可以使用Redux体系结构来处理此状态,但是可以使用
useReducer
钩子来管理状态,而不是Redux库。
如果以后您需要建立以前只是临时存储的数据的永久存储,那么您将90%准备好进行此类更改。 您要做的就是将组件连接到Redux存储库,并在其中添加相应的reducer。
问与答
如果Redux不管理所有应用程序数据,确定性是否被破坏?
不,它没有损坏。 实际上,使用Redux不能使项目具有确定性。 但是协议是。 如果您希望Redux状态是确定性的,请使用
纯函数 。 这同样适用于需要确定本地组件的临时状态的情况。
the Redux库是否应该扮演单一可靠数据源的角色?
单一可靠数据源的原理并不表示必须将应用程序状态中包含的所有数据都存储在一个位置。 该原理的含义是,状态的每个片段都应该只有一个可靠数据源。 结果,我们可以有很多状态片段,每个片段都有其自己的可靠数据源。
这意味着程序员可以决定将哪些内容传输到Redux以及将哪些内容传输到组件的状态。 状态确定数据也可以从其他来源获取。 例如,通过浏览器API,您可以使用它来处理有关正在查看的页面地址的信息。
Redux是支持应用程序状态的单个可靠数据源的出色工具。 但是,如果组件的状态仅在该组件内定位和使用,则根据定义,该状态已经具有可靠数据的单一来源-React组件的状态。
如果您将某些数据置于Redux状态,则应始终从Redux状态读取此数据。 对于Redux信息库中的所有内容,该信息库应该是可靠数据的唯一来源。
如有必要,将所有内容都置于Redux状态是完全正常的。 如果您使用需要经常更新的状态片段,或者正在谈论存储依赖状态片段被大量使用的组件的状态,则这可能会影响性能。 在性能出现问题之前,您不必担心性能。 但是,如果您担心性能问题,请尝试两种使用状态的方法,并评估它们对性能的影响。 对项目进行概要分析,并记住RAIL绩效模型。
▍是否需要使用react-redux的connect函数,还是使用钩子更好?
这取决于很多。
connect
函数创建一个适合于重复使用的高阶组件,并且钩子经过优化,可以与单个组件集成。
我需要将相同的属性连接到不同的组件吗? 如果是这样,请使用
connect
。 否则,我宁愿选择钩子。 例如,假设您有一个负责授权用户操作权限的组件:
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对此任务很方便。 与使用钩子相比,它使您可以使用更少的代码来更简洁地解决它。 但是,为了使用
connect
函数,您需要记住,它将
mapStateToProps
作为第一个参数,并将
mapStateToProps
作为第二个参数。 我们一定不要忘记这个函数可以接受函数或对象文字。 您需要知道
connect
的不同用法有何不同,并且这是一个curried函数,但是不会自动执行它的计算。
换句话说,我可以说我相信,在
connect
的开发过程中,朝着代码简洁的方向做了很多工作,但是生成的代码既不是特别可读,也不是特别方便。 如果我不需要使用多个组件,那么即使考虑到这样做会导致代码量增加,我也会更喜欢不方便的
connect
API而不是更加方便的钩子API。
▍如果将单例视为反模式,而Redux是单例,这是否意味着Redux是反模式?
不,不是。 在代码中使用单例表示该代码的可疑质量,表明其中存在共享的可变状态。 这是一个真正的反模式。 另一方面,Redux通过封装来防止共享状态的突变(您不应该在reducers外部直接更改应用程序的状态; Redux解决了更改状态的问题)并通过发送消息(只有已发送的事件对象可以导致状态更改)。
总结
Redux是否可以替换React钩子? 钩子很棒,但是它们不能代替Redux。
我们希望该材料将帮助您为React项目选择状态管理模型。
亲爱的读者们! 您是否遇到过React钩子可以代替Redux的情况?
