基于React?Hooks的小型狀態(tài)管理詳解
本文主要介紹一種基于 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;我想要的是一種輕量、簡(jiǎn)單的狀態(tài)共享方案,簡(jiǎn)簡(jiǎn)單單引用、簡(jiǎn)簡(jiǎn)單單使用。
隨著 Hooks 的誕生、流行,我的想法得以如愿。
接著介紹一下我目前在用的方案,將 Hooks 與發(fā)布/訂閱模式結(jié)合,就能實(shí)現(xiàn)一種簡(jiǎ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ù)無(wú)綁定視圖的情況下使用 */
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;
// 組件掛載后立即更新一次, 避免無(wú)法使用到第一次更新數(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 />
</>
);
};
對(duì)于復(fù)雜對(duì)象,還提供了一種方式,用于在組件中監(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ù)雜對(duì)象一般更建議使用組合派生的方式,由多個(gè)簡(jiǎn)單的狀態(tài)派生出一個(gè)復(fù)雜的對(duì)象。另外在有些時(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)共享的場(chǎng)景,我都選擇了上述方式,在 Web 項(xiàng)目和小程序 Taro 項(xiàng)目中均能使用同一套實(shí)現(xiàn),一直都比較順利。
使用感受
最后總結(jié)一下目前這種方式的幾個(gè)特點(diǎn):
1.實(shí)現(xiàn)簡(jiǎn)單,不引入其他概念,僅在 Hooks 的基礎(chǔ)上結(jié)合發(fā)布/訂閱模式,類 React 的場(chǎng)景都能使用,比如 Taro;
2.使用簡(jiǎn)單,因?yàn)闆]有其他概念,直接調(diào)用 create 方法即可得到 state 的引用,調(diào)用 state 實(shí)例上的 use 方法即完成了組件和數(shù)據(jù)的綁定;
3.類型友好,創(chuàng)建 state 時(shí)無(wú)需定義多余的類型,使用的時(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)用的場(chǎ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.支持在組件外的場(chǎng)景獲取/更新數(shù)據(jù)
7.在 SSR 的場(chǎng)景有較大局限性:state 是細(xì)碎、分散創(chuàng)建的,而且 state 的生命周期不是跟隨 React 應(yīng)用,導(dǎo)致無(wú)法用同構(gòu)的方式編寫 SSR 應(yīng)用代碼
以上,便是本文的全部?jī)?nèi)容,實(shí)際上 Hooks 到目前流行了這么久,社區(qū)當(dāng)中已有不少新型的狀態(tài)共享實(shí)現(xiàn)方式,這里僅作為一種參考。
根據(jù)以上特點(diǎn),這種方式有明顯的優(yōu)點(diǎn),也有致命的缺陷(對(duì)于 SSR 而言),但在實(shí)際使用中,可以根據(jù)具體的情況來(lái)選擇合適的方式。比如在 Taro2 的小程序應(yīng)用中,無(wú)需關(guān)心 SSR,那么我更傾向于這種方式;如果在 SSR 的同構(gòu)項(xiàng)目中,那么定還是老老實(shí)實(shí)選擇 Redux。
總之,是多了一種選擇,到底怎么用還得視具體情況而定。?
以上就是基于React Hooks的小型狀態(tài)管理詳解的詳細(xì)內(nèi)容,更多關(guān)于React Hooks 小型狀態(tài)管理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
create-react-app項(xiàng)目配置全解析
這篇文章主要為大家介紹了create-react-app項(xiàng)目配置全解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
解決React報(bào)錯(cuò)Functions are not valid as 
這篇文章主要為大家介紹了React報(bào)錯(cuò)Functions are not valid as a React child解決詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
React中用@符號(hào)編寫文件路徑實(shí)現(xiàn)方法介紹
在Vue中,我們導(dǎo)入文件時(shí),文件路徑中可以使用@符號(hào)指代src目錄,極大的簡(jiǎn)化了我們對(duì)路徑的書寫。但是react中,要想實(shí)現(xiàn)這種方式書寫文件路徑,需要寫配置文件來(lái)實(shí)現(xiàn)2022-09-09
詳解在create-react-app使用less與antd按需加載
這篇文章主要介紹了詳解在create-react-app使用less與antd按需加載,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2018-12-12

