使用react-hook 替代 react-redux

是熊大啊發表於2018-11-24

前文

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

相關文章