使用hooks编写redux

最近有一个问题比较火,是在有了react hooks之后,我们还需要redux这种状态管理工具吗?个人的观点是:仍然需要。react hooks帮助我们更好的复用代码逻辑,但是对于复杂应用而言,仍然需要redux来管理数据流,并且redux跟hooks的配合能够更好解耦组件的逻辑。最近发布的react-redux@7.1.0-alpha提供了对react hooks的支持,使用hooks将会改变过去在React应用程序中编写redux的方式。这边文章将会介绍: 为什么redux要花费很长时间才能发布对hooks的支持,它有哪些API,以及最后对迁移到hooks的一些看法。

目前存在的问题

虽然已经有第三方库提供了在hooks中使用redux的支持,但我们一直在等待redux官方支持hooks这个功能,因为现在的所有解决方案都没有比较好的性能。到目前为止,类似于useRedux或useConnect之类的库,都无法解决与性能相关的关键问题,这会导致使用hooks的组件在任何state更改时,都会重新渲染,无论我们的组件是否依赖了某个state。

主要原因是6.x.x版本的react-redux试图充分利用新的React Context API,遗憾的是它不能减少不必要的更新。这意味着,只要Context.Provider更新,那么每个Context.Consumer也必须更新。因此,一旦state更新,使用useRedux或useConnect hooks的组件将始终更新,无论这个state是否在hooks组件中用到。因此react-redux必须发布一个新的主要版本(即7.xx),在这个版本中重写了相关逻辑,将其自身与先前的Context API实现分离,并且官方提供了对hooks的支持!

对现有模式的影响

最新的redux将不需要使用connect来连接组件,而是在组件中直接使用redux提供的hooks。这对于使用redux的大型应用来说是一种非常好的提升,因为现在可以实际创建自定义的可重用hooks,而无需额外的container组件。到目前为止,我们无法创建一个hooks来从redux中读取state或者dispatch actions。因此,借助自定义hooks封装redux相关的逻辑,react将不需要直接引用redux。

并且redux提供的hooks让我们不再需要使用mapStateToProps,mapDispatchToProps和connect来维护单独的container组件和UI组件,可以立即在function组件内部读取redux中的state。此外,还可以将任何现有的自定义hooks与redux集成,而不是将通过hooks创建的state,作为参数传递给其他hooks,相当于把这块的逻辑完全抽取出去了,让我们编写的组件更纯净。

上手试试

尽管现在的redux对hooks支持还是alpha版本,但是我们可以在非生产环境上试试。

useSelector

useSelector用于从Redux存储的state中提取值并订阅该state。这基本上类似于在hooks中实现的mapStateToProps函数,但有一些小的差异:

首先,不再提供ownProps API,并且应该使用useCallback或useMemo来通过自定义逻辑获取它们。

其次,useSelector()第二个参数也是依赖数组,跟useEffect一样。如果不提供第二个参数,每次组件更新时都会重新计算;如果提供了依赖数组,只有依赖数组对应的值变更了之后,才会触发重新计算。

接下来我们看一个实际的例子:

// 以前
import React from 'react';
import { connect } from 'react-redux';

const Component = props => <div title={props.title}>{props.content}</div>;

export default connect(state => ({
  title: state.title, 
  content: state.content
}))(Component)

 

// 使用redux hooks
import React from 'react';
import { useSelector } from 'react-redux';

const Component = props => {
  const { title, content } = useSelector(state => ({
      title: state.title, 
      content: state.content
  }));
  
  return <div title={title}>{content}</div>;

除此之外,redux以前的性能优化逻辑同样保留了下来,如果当前的props跟老的props相同,则组件将不会重新渲染。

由于React Redux中使用的批处理更新的逻辑,导致同一组件中的多个useSelector()重新计算出state,只会让组件重新渲染一次。因此,我们可以自由的在组件中useSelector(),而不用担心重复渲染的情况。在上面的例子中,我们可以将单个useSelector()分成两个独立的(一个读取title,另一个读取content)useSelector(),他们在性能和渲染数量方面完全相同。

useDispatch

除了读取store中的state,还要能dispatch actions来更新store中的state,useDispatch就是这样一个API。只要组件需要触发redux action,那么这个钩子就是你需要的。不幸的是,mapDispatchToProps 被废弃掉了,所以每当你想要dispatch actions时,你需要使用dispatch(actionCreator())来调用它的action creator。如果我们初次使用这种方式,会显得有点不太习惯,因为以前都是通过connect HOC来调用被包装成prop的dispatch函数,但hooks的方式会为代码带来更多的清晰度。

遗憾的是,如果我们想要在事件处理函数里面dispatch actions,必须创建一个匿名函数,如:() => dispatch(actionCreator)。由于匿名函数的性质,这将在每次重新渲染时获得新的引用。因此,如果将这个匿名函数作为props传递给子组件组件,那么子组件将每次都重新渲染。为了优化性能,必须使该函数具有相同的引用,解决方案是在useCallback中创建这个匿名函数

import React from 'react';
import { useCallback, useDispatch } from 'react-redux';
import { increaseCounterAction } from './actions';
import ExpensiveComponent from './ExpensiveComponent';

// 常用的方式
const Component = props => {
  const dispatch = useDispatch();
  return (
    <button onClick={() => dispatch(increaseCounterAction()}>
      Increase the Counter
    </button>
  )
}

// 使用useCallback优化性能
const Component = props => {
  const dispatch = useDispatch();
  const handleIncreaseCounter = useCallback(
    () => dispatch(increaseCounterAction()), 
    [dispatch]
  );
  
  return <ExpensiveComponent onClick={handleIncreaseCounter} />
}

useStore

useStore用于获取创建的store实例。在任何需要访问store的应用中,都可以通过usestore来获取。如果出于某种原因,比如说单元测试时,想要获取不同的store,我们可以将store通过新的contextAPI传递进组件树中,就像下面这样:

import React from 'react';
import { useStore } from 'react-redux';
import OtherProvider from './OtherProvider';

const Component = props => {
  const store = useStore();
  
  return <OtherProvider store={store}>{props.children}</OtherProvider>
}

总结&权衡

上面谈到了使用hooks的方式来使用redux的好处,那我们应该将我们的connect() HOC转换为hooks吗?这里有几个需要权衡的点:

  1. 首先,这将会失去很多connect() 提供的自动引用缓存。因此会导致性能的问题,除非大量使用useCallback()来包裹(特别是对于使用dispatch的函数)。
  2. 其次,如果你的代码依赖于mapStateToProps中的ownProps,那么你最终可能会使用redux hooks编写更多的代码,而不是像connect() HOC那样可以直接拿到这个属性。
  3. 第三,我们不能像以前那样在mapDispatchToProps中,为action creator提供依赖注入

重构成本

目前,从redux HOC到redux hooks没有一条简单的迁移方案。不能简单地将单个connect() HOC替换为单个“useConnect()” hooks,因为redux并没有提供useConnect API来让我们简单的替换掉connect。在最初的alpha版本中曾经有一个useRedux() hooks,但它很快被废弃,因为其没有提供让我们缓存action creator的能力,这就会带来性能问题。

因此,现有代码中的每一个 connect(mapStateToProps,mapDispatchToProps)HOC都需要被2个分离的hooks替换,如果其中使用ownProps,则需要对mapStateToProps进行额外的重构。通过上面的内容,我们知道,要想迁移到redux的hooks上,重构成本还是蛮大的

组件开发方式

无论如何,我们仍然会采取将UI 组件和container组件分离的方式。这使我们既可以重复使用各个组件,又可以通过将这两个不同类型的组件作为不同的实体来测试。另一方面,Hooks尝试将这两者合并在一起,实质上是不在区分UI组件和container组件。这是一个新趋势,始于2018年发布hooks。原因在于:大多数情况下,UI组件仅与特定container组件相关,并且组件的可重用性基本上不是问题;另一方面,测试发生了变化,以解决这个问题,很多人正在慢慢地从单元和implements测试(Enzyme)迁移到集成和用户输出测试(React测试库)

出于这些原因,我们会根据实际应用的架构,来决定是否将组件拆分成UI组件和container组件。虽然hooks可以提高可读性(不再需要检查2个位置)和可维护性(关于文件组织和结构),但是一些成熟的代码库可能希望保持现有的组件分离模式,因为它对它们更有效。

 

原文https://zhuanlan.zhihu.com/p/70380695?utm_source=tuicool&utm_medium=referral

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值