在 React
全面转型 Hooks
的时候,会发现 react hooks
也能简单实现 flux
数据流的逻辑,让人不禁有些激动,然后又去看了下React-Redux
的官方文档,原来这事情去年就已经被包办了,React-Redux
早已全面拥抱了 Hooks
,今天就来炒一炒回锅肉,谈一谈怎么用 Hooks
的方式打开 React-Redux
。
v7.1.0开始支持 hooks
,目前是v7.2.1
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
内部把 seletedState
做了一次缓存,equalityFn是用来比较当前的seletedState
和上一次执行的seletedState
,如果不符合 equal
的条件,那么 useSeletore
就会触发组件刷新,符合条件则不做任何操作,默认情况下如果没有指定equalityFn
, React-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
进行组合使用
好处说了挺多,但是很多掘友可能还是云里雾里的,说一个 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
。
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
才行。
项目中如果有用到 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
还是很有必要的。
最后贴上文中代码的项目仓库:
PS: 文中有任何错误,欢迎掘友指正
往期精彩📌