使用react-hook 替代 react-redux

14,360

前文

react-redux主要提供的功能是将redux和react链接起来。 使用提供的connect方法可以使得任意一个react组件获取到全局的store。 实现方法是将store存放于由provider提供的context上,在调用connect时, 就可将组件的props替换, 让其可以访问到定制化的数据或者方法。

目标

本文将尝试使用最近很火爆的react-hook来替代react-redux的基础功能。

我们先将理想的特征列举出来,完成这些特性才算是替代了react-redux:

  • 全局维护一个store。
  • 任何组件都可以获取到store,最好props可以定制(mapStatetoProps)。
  • 提供可以派发action的能力(mapDispatchtoProps)。

useRudecer

先看一下内置useRudecer的官方实例能给我们带来一些什么启示:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return initialState;
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      // A reducer must always return a valid state.
      // Alternatively you can throw an error if an invalid action is dispatched.
      return state;
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, {count: initialCount});
  return (
    <div>
        Count: {state.count}
        <button onClick={() => dispatch({type: 'reset'})}>
            Reset
        </button>
        <button onClick={() => dispatch({type: 'increment'})}>+</button>
        <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <div/>
  );
}

乍一看好像react利用hook已经可以使用redux的机制了, 状态由派发的action改变,单向数据流。但是hook不会让状态共享,也就是每次useReducer保持的数据都是独立的。比如下面这个例子:


function CountWrapper() {
    return (
        <section>
            <Counter initialCount={1}/>
            <Counter initialCount={1}/>
        </setion>
        )
}

两个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];
}

StoreProvider

useReducer看来并不能帮上忙。解决全局状态的问题可以参照react-redux的做法,提供一个Provider,使用context的方式来做。 这里可以使用useContext,这个内置的hook。

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);
// do something about store.

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
}

如上就完成了hook对react-redux的功能重写,从代码量来说是简洁量不少,并且实现方式也更贴合react未来的发展方向。 可见大概率上react-redux会被hook的方式逐渐替代。本文是对redux-react-hook实现的原理讲解,想要在线尝试本文所诉内容点击这个codesandbox