react-redux主要提供的功能是将redux和react的组件关联起来。使用提供的connect方法可以使得任意一个react组件获取到全局的store。 实现方法是将store存放于由provider提供的context上,在调用connect时, 就可将组件的props替换, 让其可以访问到定制化的数据或者方法。
本文尝试使用react-hook来替代react-redux的基本功能。
react-redux的特点:
useReducer
先看一下他的内置能力是什么,官网的案例会可我们一些启示能力(也是useState的替代方案)。
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
这么看来实际上hook拥有了可以使用redux的机制,状态的派发改变的action,单向的数据流。但是hook不会状态共享,也就是每次useReducer保持是数据状态都是独立的。比如下面这个例子:
function CountWrapper() {
return (
)
}
两个Count组件内部的数据是独立的,无法互相影响,状态管理也就是无法说起。本身useReducer就是使用useState进行封装实现的。
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
StorePriovider
useReducer实际上不能替代react-redux的全局共享机制,按照react-redux的实现方式来看,是因为提供了一个Provider,使用context的方式来做的,这里使用useContext来弥补这个问题
Accepts a context object (the value returned from React.createContext) and returns the current context value, as given by the nearest context provider for the given context. When the provider updates, this Hook will trigger a rerender with the latest context value.
它本身接收的是一个React.createContext的上下文对象,当provider更新时,会传入store更新时,useContext就可以返回最新的值
import {createContext, useContext} from 'react';
const context = createContext(null);
export const StoreProvider = context.provider;
const store = useContext(context);
useDispatch
到这里我们提供了一个根组件来store, 当store更新时,我们也可以利用useContext也可以拿到最新的值。这个时候暴露出一个hook来返回store上的dispatch即可派发action,来更改state。
export function useDispatch() {
const store = useContext(Context);
return store.dispatch;
}
useStoreState
接下来着眼于组件拿到store上数据的问题,这个其实也很简单,我们都把store拿到了,编写一个自定义hook调用了store.getStore()即可拿到全局的状态。
export function useStoreState(mapState){
const store = useContext(context);
return mapState(store.getStore());
}
这里虽然是把状态拿到了,但忽略了一个非常重要的问题, 当store上的数据变化时,如何通知组件再次获取新的数据。当store变化过后,并没有和视图关联起来。另一个问题是没有关注mapState变化的情况。 针对第一个问题,我们可以利用useEffect这个内置hook,在组件mount时完成在store上的订阅,并在unmont的时候取消订阅。 mapState的变更可以使用useState来监听, 每次有变更时就执行向对应的setter方法。代码如下
export function useStoreState(mapState) {
const store = useContext(context);
const mapStateFn = () => mapState(store.getState());
const [mappedState, setMappedState] = useState(() => mapStateFn());
// If the store or mapState change, rerun mapState
const [prevStore, setPrevStore] = useState(store);
const [prevMapState, setPrevMapState] = useState(() => mapState);
if (prevStore !== store || prevMapState !== mapState) {
setPrevStore(store);
setPrevMapState(() => mapState);
setMappedState(mapStateFn());
}
const lastRenderedMappedState = useRef();
// Set the last mapped state after rendering.
useEffect(() => {
lastRenderedMappedState.current = mappedState;
});
useEffect(
() => {
// Run the mapState callback and if the result has changed, make the
// component re-render with the new state.
const checkForUpdates = () => {
const newMappedState = mapStateFn();
if (!shallowEqual(newMappedState, lastRenderedMappedState.current)) {
setMappedState(newMappedState);
}
};
// Pull data from the store on first render.
checkForUpdates();
// Subscribe to the store to be notified of subsequent changes.
const unsubscribe = store.subscribe(checkForUpdates);
// The return value of useEffect will be called when unmounting, so
// we use it to unsubscribe from the store.
return unsubscribe;
},
[store, mapState],
);
return mappedState
}
ok,从这里来看,react-hook的确可以替代react-redux,自己通过简单的封装基本完成了react-redux大部分的功能,但是,换一个场景来说,在大型web服务场景中,react-redux的优化策略和考虑的场景,包括和第三方的框架继承,例如immutable-js, redux-thunk,redux-saga。这样的优化方案,如果你需要自己做集成实现可能成本较高,同时无法考虑全面和对大型web服务的有精准的定位。而且最新的react生态产生的react-server-compoents服务端组件渲染技术,将会减少大量的数据渲染,数据处理的成本,也会在数据维护和性能优化做到一个质的飞跃。但是,中小型的web项目还需要使用react-redux么?实际上不太需要,绝大数据的中小型的web系统,没有太多的复杂交互场景和异步问题,可能只需要借助react-dom和axios就可以完成整个项目的开发了。