網(wǎng)站首頁 編程語言 正文
本文主要介紹一種基于 React Hooks 的狀態(tài)共享方案,介紹其實(shí)現(xiàn),并總結(jié)一下使用感受,目的是在狀態(tài)管理方面提供多一種選擇方式。
實(shí)現(xiàn)基于 React Hooks 的狀態(tài)共享
React 組件間的狀態(tài)共享,是一個(gè)老生常談的問題,也有很多解決方案,例如 Redux、MobX 等。這些方案很專業(yè),也經(jīng)歷了時(shí)間的考驗(yàn),但私以為他們不太適合一些不算復(fù)雜的項(xiàng)目,反而會(huì)引入一些額外的復(fù)雜度。
實(shí)際上很多時(shí)候,我不想定義 mutation 和 action、我不想套一層 context,更不想寫 connect 和 mapStateToProps;我想要的是一種輕量、簡單的狀態(tài)共享方案,簡簡單單引用、簡簡單單使用。
隨著 Hooks 的誕生、流行,我的想法得以如愿。
接著介紹一下我目前在用的方案,將 Hooks 與發(fā)布/訂閱模式結(jié)合,就能實(shí)現(xiàn)一種簡單、實(shí)用的狀態(tài)共享方案。因?yàn)榇a不多,下面將給出完整的實(shí)現(xiàn)。
import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useRef, useState, } from 'react'; /** * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.js * inlined Object.is polyfill to avoid requiring consumers ship their own * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is */ function is(x: any, y: any): boolean { return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); } const objectIs = typeof Object.is === 'function' ? Object.is : is; /** * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowEqual.js * Performs equality by iterating through keys on an object and returning false * when any key has values which are not strictly equal between the arguments. * Returns true when the values of all keys are strictly equal. */ function shallowEqual(objA: any, objB: any): boolean { if (is(objA, objB)) { return true; } if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (let i = 0; i < keysA.length; i++) { if ( !Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]]) ) { return false; } } return true; } const useForceUpdate = () => useReducer(() => ({}), {})[1] as VoidFunction; type ISubscriber<T> = (prevState: T, nextState: T) => void; export interface ISharedState<T> { /** 靜態(tài)方式獲取數(shù)據(jù), 適合在非組件中或者數(shù)據(jù)無綁定視圖的情況下使用 */ get: () => T; /** 修改數(shù)據(jù),賦予新值 */ set: Dispatch<SetStateAction<T>>; /** (淺)合并更新數(shù)據(jù) */ update: Dispatch<Partial<T>>; /** hooks方式獲取數(shù)據(jù), 適合在組件中使用, 數(shù)據(jù)變更時(shí)會(huì)自動(dòng)重渲染該組件 */ use: () => T; /** 訂閱數(shù)據(jù)的變更 */ subscribe: (cb: ISubscriber<T>) => () => void; /** 取消訂閱數(shù)據(jù)的變更 */ unsubscribe: (cb: ISubscriber<T>) => void; /** 篩出部分 state */ usePick<R>(picker: (state: T) => R, deps?: readonly any[]): R; } export type IReadonlyState<T> = Omit<ISharedState<T>, 'set' | 'update'>; /** * 創(chuàng)建不同實(shí)例之間可以共享的狀態(tài) * @param initialState 初始數(shù)據(jù) */ export const createSharedState = <T>(initialState: T): ISharedState<T> => { let state = initialState; const subscribers: ISubscriber<T>[] = []; // 訂閱 state 的變化 const subscribe = (subscriber: ISubscriber<T>) => { subscribers.push(subscriber); return () => unsubscribe(subscriber); }; // 取消訂閱 state 的變化 const unsubscribe = (subscriber: ISubscriber<T>) => { const index = subscribers.indexOf(subscriber); index > -1 && subscribers.splice(index, 1); }; // 獲取當(dāng)前最新的 state const get = () => state; // 變更 state const set = (next: SetStateAction<T>) => { const prevState = state; // @ts-ignore const nextState = typeof next === 'function' ? next(prevState) : next; if (objectIs(state, nextState)) { return; } state = nextState; subscribers.forEach((cb) => cb(prevState, state)); }; // 獲取當(dāng)前最新的 state 的 hooks 用法 const use = () => { const forceUpdate = useForceUpdate(); useEffect(() => { let isMounted = true; // 組件掛載后立即更新一次, 避免無法使用到第一次更新數(shù)據(jù) forceUpdate(); const un = subscribe(() => { if (!isMounted) return; forceUpdate(); }); return () => { un(); isMounted = false; }; }, []); return state; }; const usePick = <R>(picker: (s: T) => R, deps = []) => { const ref = useRef<any>({}); ref.current.picker = picker; const [pickedState, setPickedState] = useState<R>(() => ref.current.picker(state), ); ref.current.oldState = pickedState; const sub = useCallback(() => { const pickedOld = ref.current.oldState; const pickedNew = ref.current.picker(state); if (!shallowEqual(pickedOld, pickedNew)) { // 避免 pickedNew 是一個(gè) function setPickedState(() => pickedNew); } }, []); useEffect(() => { const un = subscribe(sub); return un; }, []); useEffect(() => { sub(); }, [...deps]); return pickedState; }; return { get, set, update: (input: Partial<T>) => { set((pre) => ({ ...pre, ...input, })); }, use, subscribe, unsubscribe, usePick, }; };
擁有 createSharedState 之后,下一步就能輕易地創(chuàng)建出一個(gè)可共享的狀態(tài)了,在組件中使用的方式也很直接。
// 創(chuàng)建一個(gè)狀態(tài)實(shí)例 const countState = createSharedState(0); const A = () => { // 在組件中使用 hooks 方式獲取響應(yīng)式數(shù)據(jù) const count = countState.use(); return <div>A: {count}</div>; }; const B = () => { // 使用 set 方法修改數(shù)據(jù) return <button onClick={() => countState.set(count + 1)}>Add</button>; }; const C = () => { return ( <button onClick={() => { // 使用 get 方法獲取數(shù)據(jù) console.log(countState.get()); }} > Get </button> ); }; const App = () => { return ( <> <A /> <B /> <C /> </> ); };
對于復(fù)雜對象,還提供了一種方式,用于在組件中監(jiān)聽指定部分的數(shù)據(jù)變化,避免其他字段變更造成多余的 render:
const complexState = createSharedState({ a: 0, b: { c: 0, }, }); const A = () => { const a = complexState.usePick((state) => state.a); return <div>A: {a}</div>; };
但復(fù)雜對象一般更建議使用組合派生的方式,由多個(gè)簡單的狀態(tài)派生出一個(gè)復(fù)雜的對象。另外在有些時(shí)候,我們會(huì)需要一種基于原數(shù)據(jù)的計(jì)算結(jié)果,所以這里同時(shí)提供了一種派生數(shù)據(jù)的方式。
通過顯示聲明依賴的方式監(jiān)聽數(shù)據(jù)源,再傳入計(jì)算函數(shù),那么就能得到一個(gè)響應(yīng)式的派生結(jié)果了。
/** * 狀態(tài)派生(或 computed) * ```ts * const count1 = createSharedState(1); * const count2 = createSharedState(2); * const count3 = createDerivedState([count1, count2], ([n1, n2]) => n1 + n2); * ``` * @param stores * @param fn * @param initialValue * @returns */ export function createDerivedState<T = any>( stores: IReadonlyState<any>[], fn: (values: any[]) => T, opts?: { /** * 是否同步響應(yīng) * @default false */ sync?: boolean; }, ): IReadonlyState<T> & { stop: () => void; } { const { sync } = { sync: false, ...opts }; let values: any[] = stores.map((it) => it.get()); const innerModel = createSharedState<T>(fn(values)); let promise: Promise<void> | null = null; const uns = stores.map((it, i) => { return it.subscribe((_old, newValue) => { values[i] = newValue; if (sync) { innerModel.set(() => fn(values)); return; } // 異步更新 promise = promise || Promise.resolve().then(() => { innerModel.set(() => fn(values)); promise = null; }); }); }); return { get: innerModel.get, use: innerModel.use, subscribe: innerModel.subscribe, unsubscribe: innerModel.unsubscribe, usePick: innerModel.usePick, stop: () => { uns.forEach((un) => un()); }, }; }
至此,基于 Hooks 的狀態(tài)共享方的實(shí)現(xiàn)介紹就結(jié)束了。
在最近的項(xiàng)目中,有需要狀態(tài)共享的場景,我都選擇了上述方式,在 Web 項(xiàng)目和小程序 Taro 項(xiàng)目中均能使用同一套實(shí)現(xiàn),一直都比較順利。
使用感受
最后總結(jié)一下目前這種方式的幾個(gè)特點(diǎn):
1.實(shí)現(xiàn)簡單,不引入其他概念,僅在 Hooks 的基礎(chǔ)上結(jié)合發(fā)布/訂閱模式,類 React 的場景都能使用,比如 Taro;
2.使用簡單,因?yàn)闆]有其他概念,直接調(diào)用 create 方法即可得到 state 的引用,調(diào)用 state 實(shí)例上的 use 方法即完成了組件和數(shù)據(jù)的綁定;
3.類型友好,創(chuàng)建 state 時(shí)無需定義多余的類型,使用的時(shí)候也能較好地自動(dòng)推導(dǎo)出類型;
4.避免了 Hooks 的“閉包陷阱”,因?yàn)?state 的引用是恒定的,通過 state 的 get 方法總是能獲取到最新的值:
const countState = createSharedState(0); const App = () => { useEffect(() => { setInterval(() => { console.log(countState.get()); }, 1000); }, []); // return ... };
5.直接支持在多個(gè) React 應(yīng)用之間共享,在使用一些彈框的時(shí)候是比較容易出現(xiàn)多個(gè) React 應(yīng)用的場景:
const countState = createSharedState(0); const Content = () => { const count = countState.use(); return <div>{count}</div>; }; const A = () => ( <button onClick={() => { Dialog.info({ title: 'Alert', content: <Content />, }); }} > open </button> );
6.支持在組件外的場景獲取/更新數(shù)據(jù)
7.在 SSR 的場景有較大局限性:state 是細(xì)碎、分散創(chuàng)建的,而且 state 的生命周期不是跟隨 React 應(yīng)用,導(dǎo)致無法用同構(gòu)的方式編寫 SSR 應(yīng)用代碼
以上,便是本文的全部內(nèi)容,實(shí)際上 Hooks 到目前流行了這么久,社區(qū)當(dāng)中已有不少新型的狀態(tài)共享實(shí)現(xiàn)方式,這里僅作為一種參考。
根據(jù)以上特點(diǎn),這種方式有明顯的優(yōu)點(diǎn),也有致命的缺陷(對于 SSR 而言),但在實(shí)際使用中,可以根據(jù)具體的情況來選擇合適的方式。比如在 Taro2 的小程序應(yīng)用中,無需關(guān)心 SSR,那么我更傾向于這種方式;如果在 SSR 的同構(gòu)項(xiàng)目中,那么定還是老老實(shí)實(shí)選擇 Redux。
總之,是多了一種選擇,到底怎么用還得視具體情況而定。?
原文鏈接:https://webfe.kujiale.com/ji-yu-react-hooks-de-xiao-xing-zhuang-tai-guan-li/
相關(guān)推薦
- 2022-05-12 Kotlin set集合去重,獲取元素可變set集合,set與list轉(zhuǎn)換
- 2022-05-22 python?使用tkinter與messagebox寫界面和彈窗_python
- 2023-07-27 el-select下拉框處理分頁數(shù)據(jù),觸底加載更多
- 2022-11-02 Android?Studio模擬器運(yùn)行apk文件_Android
- 2022-03-25 一篇文章讓你輕松理解C++中vector和list區(qū)別(c++中vector是什么)
- 2022-09-18 ASP.NET?Core實(shí)現(xiàn)文件上傳和下載_實(shí)用技巧
- 2021-12-03 Go并發(fā)編程中sync/errGroup的使用_Golang
- 2022-06-25 iOS開發(fā)CGContextRef畫圖使用總結(jié)_IOS
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支