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