用Hooks的方式打开React-Redux

3,448 阅读8分钟

React 全面转型 Hooks 的时候,会发现 react hooks 也能简单实现 flux 数据流的逻辑,让人不禁有些激动,然后又去看了下React-Redux的官方文档,原来这事情去年就已经被包办了,React-Redux 早已全面拥抱了 Hooks ,今天就来炒一炒回锅肉,谈一谈怎么用 Hooks 的方式打开 React-Redux

v7.1.0开始支持 hooks ,目前是v7.2.1 redux1.png

React Redux: github.com/reduxjs/rea…

Hooks低配版本 🚲

实现React-Redux的关键在于 Context , Context 实现了组件之间的数据共享,通过对顶层组件进行 Provider 包裹,并传入 value 值,各个层级的子组件都能通过 Context 对象拿到实现定义好的公共状态。

class组件调用 context 都是使用 this.context 来获取,而函数式组件里没有 this 指向可以使用,Hooks API里提供的 useContext ,刚好就是用来获取 context 的,还有 useReducer 提供的 dispatch 方法想必用过 Redux 的掘友也不会陌生了。

Hooks API: zh-hans.reactjs.org/docs/hooks-…

import React, { createContext, useContext, useReducer } from 'react'

export interface User {
  name: string,
  phone: string,
}

interface StateType {
  searchWords: string,
  userList: User[]
}

type ActionType = {
  type: 'UPDATE_SEARCH',
  payload: string
} | {
  type: 'UPDATE_LIST',
  payload: User[]
}

// 定义reducer分发规则
const reducer = (state: StateType, action: ActionType): StateType => {
  switch (action.type) {
    case 'UPDATE_SEARCH':
      return {
        ...state,
        searchWords: action.payload
      };
    case 'UPDATE_LIST':
      return {
        ...state,
        userList: action.payload
      };
    default:
      break;
  }
  return state
}

// 创建初始化state,dispatch
const initState: StateType = {
  searchWords: '',
  userList: []
}
const initDispatch = (action: ActionType): void => {}

// 创建上下文context
const StoreCtx = createContext(initState);
const DispatchCtx = createContext(initDispatch);

const ReduxHooks: React.FC<{ children: JSX.Element }> = ({
  children
}) => {
  // 创建reducer并注入全局state和dispatch
  const [state, dispatch] = useReducer(reducer, initState);

  return (
    <DispatchCtx.Provider value={dispatch}>
      <StoreCtx.Provider value={state}>
        {children}
      </StoreCtx.Provider>
    </DispatchCtx.Provider>
  );
};

type SelectType = StateType | User[] | string

export const useSelector = (selector: (params: StateType) => SelectType): SelectType => {
  const store = useContext(StoreCtx)
  return selector(store)
}

export const useDispatch = () => {
  const dispatch = useContext(DispatchCtx)
  return dispatch
}

export default ReduxHooks

分析上述代码,这里通过绑定 useReducer 的 state 和 dispatch 到 Context ,注入到被 ReduxHooks 组件包裹的所有子组件里,然后提供两个自定义的 Hooks 来让子组件能在函数式组件里拿到 store 和 dispatch ,这里把 store 和 dispatch 都当做了context内的动态数据来管理。

// 顶层父组件
const Parrent = () => {
  return (
    <ReduxHooks>
      <>
        <UserSearch />
        <UserList />
      </>
    </ReduxHooks>
  )
}

const Child1 = () => {
  const searchWords = useSelector(state => state.searchWords) as string
  const userList = useSelector(state => state.userList) as User[]
  
  return (
    <div>{searchWords}</div>
  )
}

const Child2 = ()  => {
  const searchWords = useSelector(state => state.searchWords) as string
  const dispatch = useDispatch()

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: 'UPDATE_SEARCH',
      payload: e.target.value
    })
  }

  return (
    <div>
      <input type="text" value={searchWords} onChange={handleChange} />
    </div>
  )
}

使用起来也是比较方便的,父组件里用 ReduxHooks 作为根节点包裹,子组件里调用 useSelector 和 useDispatch , useSelector 传入一个纯函数来获取 store 里的值,这样避免了直接操作 store 引起脏数据。

但这毕竟只是低配版本的 Redux ,仅仅实现了 flux 数据流的基本操作,没有更好的优化,存在以下几个问题:

  • store 里任何数据改变都会导致有引用数据的子组件更新,无法用 memo 优化
  • 无法使用 Redux 中间件, redux-saga 等
  • 不利于 Redux DevTools 调试

PS: 如果看TypeScript代码有些吃力,不妨先看下我之前写的一篇文章:

TypeScript怎么写React Hooks | 掘金技术征文-双节特别篇

React-Redux 🚀

Hooks 低配版本的 Redux 虽然存在很多问题,但是能够快速让我们了解 Redux 的核心概念,然后一些吃力不讨好的苦活累活还是得交给 React-Redux 这个库来干,React-Redux提供的 Hooks API 也足够强大,接下来就来看看 React-Redux 是怎么使用的。

基础用法

import { Provider } from 'react-redux'
import { createStore } from 'redux'

// reducer和initState沿用上文的Hooks版本
const store = createStore(reducer, initState)

const ReduxProvider: React.FC<{ children: JSX.Element }> = ({
  children
}) => {
  return (
    <Provider store={store}>
      {children}
    </Provider>
  )
}

这里我们用官方的 redux 组合拳实现了 ReduxProvider 根组件,使用方法也和之前的低配版本相同,分析来看这边用 createStore 专门用来生产 state 和 dispatch ,相当于包揽了 createContext 和 useReducer 的活, Provider 直接由 React-Redux 来提供,并不需要开发者关心如何绑定 context 。

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { StateType } from '@/utils/ReduxProvider'

const Child2: React.FC = ()  => {
  const searchWords = useSelector((state: StateType) => state.searchWords)
  const dispatch = useDispatch()

  return (
    <div
      onClick={() => {
        dispatch({
          type: 'UPDATE_SEARCH',
          payload: 'another'
        })
      }}
    >
      {searchWords}
    </div>
  )
}

这里使用 useSelector 也是传入一个纯函数,纯函数接收的参数就是 Redux 的 store ,这么一看好像子组件还是不能用 memo 来优化性能,客官莫慌,接下来就来剖析一下 useSelector 。

useSelector

低配 hooks redux 会存在性能问题是因为每次 Provider 的 value 发生变化的时候,子组件注入的 useSelector 钩子也会被触发执行,从而导致子组件的重新渲染,所以想要完美解决多余的重复刷新,还是得从useSelector 下手。巧的是useSelector 就提供了一个 equalityFn 来帮助优化。

源码里的注释说明: @param {Function} selector the selector function @param {Function=} equalityFn the function that will be used to determine equality

源码传送门--useSelector

稍微看下源码就可以发现, useSelector 内部把 seletedState 做了一次缓存,equalityFn是用来比较当前的seletedState和上一次执行的seletedState,如果不符合 equal 的条件,那么 useSeletore 就会触发组件刷新,符合条件则不做任何操作,默认情况下如果没有指定equalityFnReact-Redux 会使用全等 === 来进行严格比较。

const [, forceRender] = useReducer(s => s + 1, 0) // 调用forceRender进行组件刷新

const refEquality = (a, b) => a === b // 默认的equal函数

但是如果是一些复杂的引用数据类型,默认情况的严格比较 === 就不够看了, React-Redux 提供了 shallowEqual 函数来用作对象,数组这些数据类型的浅比较。同时,官方文档也说明了 Lodash 的 _.isEqual() 和 Immutable.js 的比较函数适用于这种场景。

既然提到 redux 的性能优化,那么还有个优秀的轮子不得不提,那就是 reselect 。 reselect 的三个特点:

  • Redux 存储粒度最小的状态数据,组合派生的活让 reselect 来干
  • reselect 会缓存 selector ,直到依赖的参数发生改变才会重新计算
  • 可以将其他的 selector 进行组合使用

react_hooks2.png 好处说了挺多,但是很多掘友可能还是云里雾里的,说一个 redux 中简单的功能就能理解 reselector 了, connect 这个 HOC 想必掘友都不会陌生, connect 通过高阶组件的方式把 redux 中 store 的数据绑定在了子组件的 props 上,这样才让子组件通过 props 即可拿到 redux 公共的状态数据。而调用 connect 接收的第一个参数就是 mapStateToProps ,mapStateToProps 这个纯函数其实就是 selector 。

这样一看,reselect 的作用就很清晰了,reselect保证了依赖于 redux 数据流的selector 能够得到最大程度的缓存,缓存就意味着减少复杂逻辑的执行,从而保证了性能。

import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const lenOfSearch = createSelector(
  (state: StateType) => state.searchWords,
  words => words.length
)

const Child1: React.FC = ()  => {
  const len = useSelector(lenOfSearch)

  return (
    <div>
      {len}
    </div>
  )
}

上述代码就演示了 reselect 结合 hooks 在 useSelector 中的简单用法,如果 searchWords 不发生改变, Child1 组件里的 len 就不会更新, Child1 也自然不会更新,用起来和 useMemo , vue 的 computed 有几分相像,都是对复杂计算做缓存,当然还有其他更高级的用法这里不作展开,可以参考一下官方的仓库。

reselect: github.com/reduxjs/res…

Provider

还有一个比较核心的 API 就是 React-Redux  的 Provider ,研究 React-Redux 的 Provider 源码会发现其核心的概念还是 React 的 createContext 。

export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)

但是在此基础上, React-Redux 也做了一些性能的优化,额外的订阅,自定义 context 的功能,基本上也是全面拥抱了 Hooks API 。

源码传送门--Provider

function Provider({ store, context, children }) {
  // 缓存contextValue,store发生改变就会更新subscription
  const contextValue = useMemo(() => {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])

  const previousState = useMemo(() => store.getState(), [store])

  useEffect(() => {
    const { subscription } = contextValue
    subscription.trySubscribe()

    // 数据发生改变随即触发subscription的通知
    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  // 也可自定义context上下文
  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

自定义 context 用起来和上面提到的 Hooks 低配版本有点像,下面代码就对 ReactHooks 做了一次改写,可以用来实现局部的公共状态管理,与全局提供的 useSelector 区分开来。

import {
  Provider,
  createSelectorHook
} from 'react-redux'

const StoreCtx = React.createContext(null);
const myStore = createStore(reducer);

// 导出绑定自定义context的useSelector hooks
export const useCustomSelector = createSelectorHook(StoreCtx);

const ReduxHooks: React.FC<{ children: JSX.Element }> = ({
  children
}) => {
  return (
    <Provider context={StoreCtx} store={myStore}>
      {children}
    </Provider>
  );
};

export default ReduxHooks

Dva的升级

当初学习 redux 的时候,还顺带学习了阿里的 dva.js , dva.js 极大程度简化了 redux 配置的功能,让开发者只去关心业务逻辑,现在虽然官方已经不怎么维护 dva.js 了,而把重心放在了新的 umi.js 框架,同时也在插件集合中增加了 dva.js 的插件,文档里其实也支持了 hooks 的用法,但是需要升级到 2.6.x 才行。 屏幕快照 2020-10-10 下午3.42.22.png 项目中如果有用到 dva ,或者 umi 的话,可以留意一下版本。

关于model如何定义,可以看下文档的介绍,这里简单演示一下dva插件提供的useSelector使用方式。

import React from 'react';
import { useSelector } from 'umi'
import { IndexModelState } from '@/models/index'

export default () => {
  const name = useSelector((state: {
    index: IndexModelState
  }) => state.index.name)

  return (
    <div>
      <h1>Page index</h1>
      <span>{name}</span>
    </div>
  );
}

开启 dva 的插件配置后,然后需要运行一下 umi generate tmp ,才能进行引入 useSelector ,这里吐槽一下 dva 的类型推导,这里需要引入一下之前定义好的 IndexModelState 接口才能在后续的代码中提示类型推导。

umi插件: umijs.org/zh-CN/plugi…

结束

最近 facebook 出了一个新的状态管理框架 Recoil ,还处于实验阶段,毕竟是官方的轮子,还是挺看好的,但是目前 Redux 在 react 生态圈里已经非常成熟了,短时间内被替换掉还是不太可能,所以学好 Redux 还是很有必要的。

最后贴上文中代码的项目仓库:

github

PS: 文中有任何错误,欢迎掘友指正

往期精彩📌