使用70行程式碼配合hooks重新實現react-redux

超級大柱子發表於2018-11-09

原由

react-hooks 是 react 官方新的編寫推薦,我們很容易在官方的 useReducer 鉤子上進行一層很簡單的封裝以達到和以往 react-redux \ redux-thunk \ redux-logger 類似的功能,並且大幅度簡化了宣告。

react-hooks 的更多資訊請閱讀 reactjs.org/hooks;

先看看原始碼

這 70 行程式碼是一個完整的邏輯, 客官可以先閱讀,或許後續的說明文件也就不需要閱讀了。

  • 簡易的實現了 react-redux, redux-thunk 和 redux-logger
  • 預設使用 reducer-in-action 的風格, 也可宣告傳統的 reducer 風格
import React from 'react';

function middlewareLog(lastState, nextState, action, isDev) {
  if (isDev) {
    console.log(
      `%c|------- redux: ${action.type} -------|`,
      `background: rgb(70, 70, 70); color: rgb(240, 235, 200); width:100%;`,
    );
    console.log('|--last:', lastState);
    console.log('|--next:', nextState);
  }
}

function reducerInAction(state, action) {
  if (typeof action.reducer === 'function') {
    return action.reducer(state);
  }
  return state;
}

export default function createStore(params) {
  const { isDev, reducer, initialState, middleware } = {
    isDev: false,
    reducer: reducerInAction,
    initialState: {},
    middleware: params.isDev ? [middlewareLog] : undefined,
    ...params,
  };
  const AppContext = React.createContext();
  const store = {
    isDev,
    _state: initialState,
    useContext: function() {
      return React.useContext(AppContext);
    },
    dispatch: undefined,
    getState: function() {
      return store._state;
    },
    initialState,
  };
  let isCheckedMiddleware = false;
  const middlewareReducer = function(lastState, action) {
    let nextState = reducer(lastState, action);
    if (!isCheckedMiddleware) {
      if (Object.prototype.toString.call(middleware) !== '[object Array]') {
        throw new Error("react-hooks-redux: middleware isn't Array");
      }
      isCheckedMiddleware = true;
    }
    for (let i = 0; i < middleware.length; i++) {
      const newState = middleware[i](store, lastState, nextState, action);
      if (newState) {
        nextState = newState;
      }
    }
    store._state = nextState;
    return nextState;
  };

  const Provider = props => {
    const [state, dispatch] = React.useReducer(middlewareReducer, initialState);
    if (!store.dispatch) {
      store.dispatch = async function(action) {
        if (typeof action === 'function') {
          await action(dispatch, store._state);
        } else {
          dispatch(action);
        }
      };
    }
    return <AppContext.Provider {...props} value={state} />;
  };
  return { Provider, store };
}
複製程式碼

reducer-in-action 風格

reducer-in-action 是一個 reducer 函式,這 6 行程式碼就是 reducer-in-action 的全部:

function reducerInAction(state, action) {
  if (typeof action.reducer === 'function') {
    return action.reducer(state);
  }
  return state;
}
複製程式碼

它把 reducer 給簡化了,放置到了每一個 action 中進行 reducer 的處理。我們再也不需要寫一堆 switch,再也不需要時刻關注 action 的 type 是否和 redcer 中的 type 一致。

reducer-in-action 配合 thunk 風格,可以非常簡單的編寫 redux,隨著專案的複雜,我們只需要編寫 action,會使得專案結構更清晰。

安裝

安裝 react-hooks-redux, 需要 react 版本 >= 16.7

yarn add react-hooks-redux
複製程式碼

使用

我們用了不到 35 行程式碼就宣告瞭一個完整的 react-redux 的例子, 擁抱 hooks。

import React from 'react';
import ReactHookRedux from 'react-hooks-redux';

// 通過 ReactHookRedux 獲得 Provider 元件和一個 sotre 物件
const { Provider, store } = ReactHookRedux({
  isDev: true, // 列印日誌
  initialState: { name: 'dog', age: 0 },
});

function actionOfAdd() {
  return {
    type: 'add the count',
    reducer(state) {
      return { ...state, age: state.age + 1 }; // 每次需要返回一個新的 state
    },
  };
}

function Button() {
  function handleAdd() {
    store.dispatch(actionOfAdd()); //dispatch
  }
  return <button onClick={handleAdd}>add</button>;
}

function Page() {
  const state = store.useContext();
  return (
    <div>
      {state.age} <Button />{' '}
    </div>
  );
}

export default function App() {
  return (
    <Provider>
      <Page />
    </Provider>
  );
}
複製程式碼

總結一下:

  • 準備工作
    • 使用 ReactHookRedux 建立 Provider 元件 和 store 物件
    • 使用 Provide r 包裹根元件
  • 使用
    • 在需要使用狀態的地方 使用 store.useContext() 獲取 store 中的 state
    • 使用 store.dispatch(action()) 派發更新

我們閱讀這個小例子會發現,沒有對元件進行 connect, 沒有編寫 reducer 函式, 這麼簡化設計是為了迎合 hooks, hooks 極大的簡化了我們編寫千篇一律的類别範本,但是如果我們還是需要對元件進行 connect, 我們又回到了編寫模板程式碼的老路。

middleware 的編寫

絕大部分情況,你不需要編寫 middleware, 不過它也極其簡單。middleware 是一個一維陣列,陣列中每個物件都是一個函式, 傳入了引數並且如果返回的物件存在, 就會替換成 nextState 並且繼續執行下一個 middleware。

我們可以使用 middleware 進行列印日誌、編寫 chrome 外掛或者二次處理 state 等操作。

我們看看 middleware 的原始碼:

let nextState = reducer(lastState, action);
for (let i = 0; i < middleware.length; i++) {
  const newState = middleware[i](lastState, nextState, action, isDev);
  if (newState) {
    nextState = newState;
  }
}
return nextState;
複製程式碼

效能和注意的事項

效能(和實現上)上最大的區別是,react-hooks-redux 使用 useContext 鉤子代替 connect 高階元件進行 dispatch 的派發。

在傳統的 react-redux 中,如果一個元件被 connect 高階函式進行處理,那麼當 dispatch 時,這個元件相關的 mapStateToProps 函式就會被執行,並且返回新的 props 以啟用元件更新。

而在 hooks 風格中,當一個元件被宣告瞭 useContext() 時,context 相關聯的物件被變更了,這個元件會進行更新。

理論上效能和 react-redux 是一致的,由於 hooks 相對於 class 有著更少的宣告,所以應該會更快一些。

所以,我們有節制的使用 useContext 可以減少一些元件被 dispatch 派發更新。

如果我們需要手動控制減少更新 可以參考 useMemo 鉤子的使用方式進行配合。

如果不希望元件被 store.dispatch() 派發更新,僅讀取資料可以使用 store.getState(), 這樣也可以減少一些不必要的元件更新。

以上都是理論分析,由於此庫和此文件是一個深夜的產物,並沒有去做效能上的基準測試,所以有人如果願意非常歡迎幫忙做一些基準測試。

其他例子

隨著工作的進展,完善了一些功能, 程式碼量也上升到了300行,有興趣的可以去倉庫看看:

  • subscribe 新增監聽
  • 如使用 autoSave 約定進行 state 的快取和讀取
  • middlewareLog 可以列印 immutable 物件等和狀態管理相關的功能

非同步 action 並且快取 state 到瀏覽器的例子

import React from 'react';
import ReactHookRedux, {
  reducerInAction,
  middlewareLog,
} from 'react-hooks-redux';

// 通過 ReactHookRedux 獲得 Provider 元件和一個 sotre 物件
const { Provider, store } = ReactHookRedux({
  isDev: true, // default is false
  initialState: { count: 0, asyncCount: 0 }, // default is {}
  reducer: reducerInAction, // default is reducerInAction 所以可省略
  middleware: [middlewareLog], // default is [middlewareLog] 所以可省略
  actions: {}, // default is {} 所以可省略
  autoSave: {
    item: 'localSaveKey',
    keys: ['user'], // 需要快取的欄位
  },
});

// 模擬非同步操作
function timeOutAdd(a) {
  return new Promise(cb => setTimeout(() => cb(a + 1), 500));
}

const actions = {
  // 如果返回的是一個function,我們會把它當成類似 react-thunk 的處理方式,並且額外增加一個ownState的物件方便獲取state
  asyncAdd: () => async (dispatch, ownState) => {
    const asyncCount = await timeOutAdd(ownState.asyncCount);
    dispatch({
      type: 'asyncAdd',
      // if use reducerInAction, we can add reducer Function repeat reducer
      reducer(state) {
        return {
          ...state,
          asyncCount,
        };
      },
    });
  },
};

function Item() {
  const state = store.useContext();
  return <div>async-count: {state.asyncCount}</div>;
}

function Button() {
  async function handleAdd() {
    // 使用 async dispatch
    await store.dispatch(actions.asyncAdd());
  }
  return <button onClick={handleAdd}>add</button>;
}

export default function App() {
  return (
    <Provider>
      <Item />
      <Button />
    </Provider>
  );
}
複製程式碼

使用 immutableJS 配合 hooks 減少重渲染的例子

import React, { useCallback } from 'react';
import ReactHookRedux from 'react-hooks-redux';
import { Map } from 'immutable';

const { Provider, store } = ReactHookRedux({
  initialState: Map({ products: ['iPhone'] }), // 請確保immutable是一個Map
  isDev: true, // 當發現物件是 immutable時,middleware會遍歷屬性,使用getIn做淺比較列印 diff的物件
});

function actionAddProduct(product) {
  return {
    type: 'add the product',
    reducer(state) {
      return state.update('products', p => {
        p.push(product);
        return [...p];
      });
    },
  };
}

let num = 0;
function Button() {
  function handleAdd() {
    num += 1;
    store.dispatch(actionAddProduct('iPhone' + num)); //dispatch
  }
  return <button onClick={handleAdd}>add-product</button>;
}

function Page() {
  const state = store.useContext();
  // 從immutable獲取物件,如果products未改變,會從堆中獲取而不是重新生成新的陣列
  const products = state.get('products');

  return useCallback(
    <div>
      <Button />
      {products.map(v => (
        <div>{v}</div>
      ))}
    </div>,
    [products], // 如果products未發生改變,不會進行進行重渲染
  );
}

export default function App() {
  return (
    <Provider>
      <Page />
    </Provider>
  );
}
複製程式碼

謝謝閱讀。

相關文章