使用 React Hooks + Context 打造一個類vuex語法的簡單數據管理。

  • 2020 年 4 月 10 日
  • 筆記

React Hooks 是目前社區非常火熱的一個新的特性,vue 3.0也引入了hooks,這個特性 在 React16.8 版本正式發佈。

這篇文章不過多介紹hooks的基礎用法,相關的文章一大堆,個人非常推薦把精讀周刊里關於hooks的文章全部看一遍。 前端精讀周刊

最近公司做了一個新項目,是後台管理系統,我們沒有引入redux,但是其實在某些比較複雜的頁面級模塊中,組件拆分的層級非常深,所以我想到了可以利用React的Context這個api進行跨層級的數據傳遞,利用useReducer去做一個簡單的store來統一操作模塊的數據。

基礎用法 Context配合useReducer

先貼一個利用Context配合useReducer的簡單示例

定義Store

const CountContext = React.createContext();    const initialState = 0;  const reducer = (state, action) => {    switch (action.type) {      case 'increment': return state + 1;      case 'decrement': return state - 1;      case 'set': return action.count;      default: throw new Error('Unexpected action');    }  };    const CountProvider = ({ children }) => {    const contextValue = useReducer(reducer, initialState);    return (      <CountContext.Provider value={contextValue}>        {children}      </CountContext.Provider>    );  };    const useCount = () => {    const contextValue = useContext(CountContext);    return contextValue;  };  複製代碼

組件中使用方法:

const Counter = () => {    const [count, dispatch] = useCount();    return (      <div>        {count}        <button onClick={() => dispatch({ type: 'increment' })}>+1</button>        <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>        <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>      </div>    );  };    const Page = () => (    <>      <CountProvider>        <Counter />        <Counter />      </CountProvider>      <CountProvider>        <Counter />        <Counter />      </CountProvider>    </>  );  複製代碼

很好,很方便,但是useReducer更適用於小型的模塊,我們肯定不會每個模塊每次使用store都去寫這麼一段重複的Provider定義代碼,所以我們要找出這個模式的痛點,然後進行一些封裝~

基礎用法的不足

  • 每次引入都要走 createContext -> 定義Provider -> 找一個合適的地方把Provider放上去 這一系列流程。
  • reducer的寫法 switch case不是很友好,可讀性相對較差。
  • 沒有支持異步處理
  • 不支持自動計算依賴state變化的值。

這些缺點是在項目開發中真實體驗到的,所以還是有必要去做封裝的。

期望的使用方式

編寫 store

// store.js  import initStore from 'react-hook-store'    const store = {    // 初始狀態    initState: {      count: 0,    },    // 同步操作 必須返回state的拷貝值    mutations: {      // 淺拷貝state      add(payload, state) {        return Object.assign({}, state, { count: state.count + 1 })      },    },    // 異步操作,擁有dispatch的執行權    actions: {      async asyncAdd(payload, { dispatch, state, getters }) {        await wait(1000)        dispatch({ type: 'add' })        // 返回的值會被包裹的promise resolve        return true      },    },    // 計算屬性 根據state里的值動態計算    // 在頁面中根據state值的變化而動態變化    getters: {      countPlusOne(state) {        return state.count + 1      },    },  }    export const { connect, useStore } = initStore(store)  

在頁面引用

// page.js  import React, { useMemo } from 'react'  import { Spin } from 'antd'  import { connect, useStore } from './store.js'    function Count() {    const { state, getters, dispatch } = useStore()    const { countPlusOne } = getters    const { loadingMap, count } = state    // loadingMap是內部提供的變量 會監聽異步action的起始和結束    // 便於頁面顯示loading狀態    // 需要傳入對應action的key值    // 數組內可以寫多項同時監聽多個action    // 靈感來源於dva    const loading = loadingMap.any(['asyncAdd'])      // 同步的add    const add = () => dispatch({ type: 'add' })      // 異步的add    const asyncAdd = () => dispatch.action({ type: 'asyncAdd' })    return (      <Spin spinning={loading}>        <span>count is {count}</span>        <span>countPlusOne is {countPlusOne}</span>        <button onClick={add}>add</button>        <button onClick={asyncAdd}>async add</button>          {/** 性能優化的做法 * */}        {useMemo(          () => (            <span>只有count變化會重新渲染 {count}</span>          ),          [count]        )}      </Spin>    )  }    // 必須用connect包裹 內部會保證Context的Provider在包裹Count的外層  export default connect(Count)  複製代碼

適用場景

比較適用於單個比較複雜的小模塊,個人認為這也是 react 官方推薦 useReducer 和 context 配合使用的場景。 由於所有使用了 useContext 的組件都會在 state 發生變化的時候進行更新(context 的弊端),推薦渲染複雜場景的時候配合 useMemo 來做性能優化。

預覽地址

codesandbox.io/s/react-hoo…

源碼地址

github.com/sl1673495/r…

總結

這是一次簡單的封裝嘗試,雖然已經在生產環境跑起來了,但是覆蓋的場景還是比較少,如果有優化的建議和吐槽都歡迎提出來~ 如果有小夥伴對實現的過程感興趣的話,也可以留言,後續可以增加源碼的相關解析。