使用70行代碼配合hooks從新實現react-redux

起因

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

react-hooks 的更多信息請閱讀 reactjs.org/hooks;react

先看看源碼

這 70 行代碼是一個完整的邏輯, 客官能夠先閱讀,或許後續的說明文檔也就不須要閱讀了。git

  • 簡易的實現了 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 的所有:github

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

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

reducer-in-action 配合 thunk 風格,能夠很是簡單的編寫 redux,隨着項目的複雜,咱們只須要編寫 action,會使得項目結構更清晰。redux

安裝

安裝 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>
  );
}
複製代碼

謝謝閱讀。

相關文章
相關標籤/搜索