天天看點

使用 React Hooks + Context 打造一個類vuex文法的簡單資料管理。

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…

總結

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