Recoil:Facebook 新一代的 React 狀態(tài)管理庫
本文主要介紹facebook出的狀態(tài)管理庫Recoil(非react官方)。
其優(yōu)點
避免類似Redux和Mobx這樣的庫帶來的開銷。 規(guī)避Context 的局限性。
其缺點:
目前只支持hooks 。 處于實驗階段,穩(wěn)定性有待觀察。
引言
Redux
放一張很熟悉的圖。redux的狀態(tài)管理如下圖所示。
Mobx
Observable State, 所有可以改變的值。
Derivation:
Computed Value(又稱Derivation), 是可以用純函數(shù)從當(dāng)前可觀察狀態(tài)中衍生出的值。 Reaction, 與Computed Value類似也是基于Observable State 。當(dāng)狀態(tài)改變時需要自動發(fā)生的副作用,用來連接命令式編程和響應(yīng)式編程,最終都需要實現(xiàn)I/O操作,例如發(fā)送請求,更新頁面等。
Action, 所有修改Observable State的動作,用戶事件,后端數(shù)據(jù)推送等。

注:可變數(shù)據(jù)流。(如果需要Mutable方式管理react狀態(tài),可以參考Mobx中文文檔[1])。
兩者聯(lián)系與區(qū)別:
編程方式:redux 更加偏向函數(shù)式編程,Mobx思想上更加偏向面向?qū)ο缶幊毯晚憫?yīng)式編程。 數(shù)據(jù)存儲方式不同:Redux將數(shù)據(jù)保存在單一store中,Mobx將數(shù)據(jù)保存在分散的多個store中。 狀態(tài)存儲的形式: redux存儲的js原生對象形式:需要手動追蹤狀態(tài)的變化。 Mobx會將該狀態(tài)包裝成一個可觀察對象,并自動追蹤這個狀態(tài)的更新。 數(shù)據(jù)是否是可變狀態(tài):Redux更多的偏向使用不可變狀態(tài),不能直接去修改它,而是應(yīng)該使用純函數(shù)返回一個新的狀態(tài)。Mobx中的狀態(tài)是可以直接修改的。https://juejin.cn/post/6844903797085437966[2]
State 與 Content
問題: State 與 Content 存在的問題
場景:有 List 和 Canvas 兩個組件,List 中節(jié)點更新,Canvas 中對應(yīng)的節(jié)點也更新。
第一種方法:將 State 傳到公共父節(jié)點。
缺點: 會全量re-render。
第二種方法:給父節(jié)點加 Provider 在子節(jié)點加 Consumer,不過每多加一個 item 就要多一層 Provider。

一. 介紹:
在構(gòu)建一個react應(yīng)用時一個令人頭痛的問題是狀態(tài)管理。雖然目前有較為成熟的狀態(tài)管理庫如redux和Mobx,使用他們所帶來的開銷也是難以估量的。當(dāng)然最理想的方法是使用react來進行狀態(tài)管理。
但是這帶來了以下三個問題。組件狀態(tài)只能與其祖先組件進行共享,這可能會帶來組件樹中大量的重繪開銷。Context 只能保存一個特定值而不是與其 Consumer 共享一組不確定的值。
以上兩點導(dǎo)致組件樹頂部組件(狀態(tài)生產(chǎn)者)與組件樹底部組件(狀態(tài)消費者)之間的代碼拆分變得非常困難 Recoil 在組件樹中定義了一個正交且內(nèi)聚的單向圖譜。狀態(tài)變更通過以下方法從圖譜的底部(atoms)通過純函數(shù)(selectors)進入組件。
思想:將組件中的狀態(tài)單獨抽離出來,構(gòu)成一個獨立于組件的狀態(tài)樹,樹的底部是atom通過selectors進入組件。
如圖所示。提供了一些無依賴的方法,這些方法像 React 局部狀態(tài)一樣暴露相同的 get/set 接口(簡單理解為 reducers 之類的概念亦可)。
我們能夠與一些 React 新功能(比如并發(fā)模式)兼容。狀態(tài)定義是可伸縮和分布式的,代碼拆分成為可能。
不用修改組件即可派生數(shù)據(jù)狀態(tài)。派生數(shù)據(jù)狀態(tài)支持同步和異步。把跳轉(zhuǎn)看作一級概念,甚至可以對鏈接中的狀態(tài)流轉(zhuǎn)進行編碼。
所以可以簡單地使用向后兼容的方式來持久化整個應(yīng)用的狀態(tài),應(yīng)用變更時持久化狀態(tài)也可以因此得以保留。
可以把 Atom 想象為為一組 state 的集合,改變一個 Atom 只會渲染特定的子組件,并不會讓整個父組件重新渲染。與Redux和Mobx相比,redux與Mobx 不能訪問React內(nèi)部調(diào)度的程序。而recoil在后臺使用React本身的狀態(tài)。
二. 主要概念
Atoms - 共享狀態(tài)
組件可訂閱的最小狀態(tài)單元-可被定義和更新類似于setState中的state。(一般定義一些基礎(chǔ))
const todoListState = atom({
key: 'todoListState', //key是RecoilRoot 作用域內(nèi)唯一的
default: [],
});
Selector(derived state) - 純函數(shù)
一個selector代表一個派生的狀態(tài)(由基礎(chǔ)的狀態(tài)atom派生)。入?yún)⑹茿toms/Selector類型的純函數(shù)。當(dāng)它的上游改變時,它會自動更新。其使用方法和Atom基本類似。
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
set: ({get, set},newValue) => {
return set('',newValue)
},
});
Key: 與atom 的key一樣的作用具有唯一性。 Get屬性:定義如何取值。是一個計算函數(shù),可以使用get字段來訪問輸入的Atom和Selector。當(dāng)其所依賴的狀態(tài)更新時,改狀態(tài)也會跟著更新。 Set :返回新的可寫狀態(tài)的可選函數(shù)。
注:只有同時具有g(shù)et和set的selector才具備可讀寫屬性。set: 設(shè)置原子值的函數(shù)。
相關(guān)hooks
useRecoilValue():對Atom/Selector進行讀操作(有些Selector只有可讀屬性沒有可寫屬性)。
function TodoList() {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
useSetRecoilState():對Atom/Selector進行寫操作。
其他相關(guān)hooks
function TodoItemCreator() {
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
const addItem = () => {
setTodoList((oldTodoList) => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue('');
};
const onChange = ({target: {value}}) => {
setInputValue(value);
};
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
);
}
// utility for creating unique Id
let id = 0;
function getId() {
return id++;
}
useRecoilState(): 對原子進行讀寫操作。 useResetRecoilState():重置原子的默認值。
useSetRecoilState 與 useRecoilState 的不同之處在于,數(shù)據(jù)流的變化不會導(dǎo)致組件 Rerende, useSetRecoilState僅僅是寫入該原子, 沒有訂閱該原子以及原子的更新。
注:所有的Atom都是可讀寫的狀態(tài)。
<RecoilRoot ...props>
全局的數(shù)據(jù)流管理需要在RecoilRoot作用域上才可以,被嵌套時最內(nèi)層會嵌套外曾的作用域。
三. 異步處理:
Sync
同步狀態(tài)下,只要上游的數(shù)據(jù)變了它就會自動改變。如上文所示。
Async
只需要get函數(shù)返回的是一個promise即可。Recoil 對于異步處理是需要與React Suspense[3] 一起來處理異步的數(shù)據(jù)。如果任何依賴項發(fā)生更改,將重新計算選擇器并執(zhí)行新查詢。會對結(jié)果進行緩存,如果輸入一樣將不會進行查詢,對相同的輸入也只會進行一次查詢。
例子:
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
}
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
//處于pending狀態(tài)會將promise拋出,交給suspense來處理。
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}
異步狀態(tài)可以被 Suspence 捕獲。 異步過程報錯可以被ErrorBoundary 捕獲。
不使用Suspence
除了使用Suspence來處理異步的selector,還可以使用useRecoilValueLoadable()這個Api在當(dāng)前組件。
function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}
可以通過state的狀態(tài)來讀取到異步的請求。
依賴外部變量進行查詢
有些時候需要使用其他參數(shù)(而不是Atom/Select)來進行數(shù)據(jù)查詢。
const userNameQuery = selectorFamily({
key: 'UserName',
get: (userID) => async ({get}) => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function UserInfo({userID}) {
const userName = useRecoilValue(userNameQuery(userID));
return <div>{userName}</div>;
}
四. Utils
atomFamily() 與autom()類似,不同的是atomFamily返回一個函數(shù),該函數(shù)接受一個參數(shù)。可以根據(jù)這個參數(shù)來提供不同的Atom.
const elementPositionStateFamily = atomFamily({
key: 'ElementPosition',
default: [0, 0],
});
function ElementListItem({elementID}) {
const position = useRecoilValue(elementPositionStateFamily(elementID));
return (
<div>
Element: {elementID}
Position: {position}
</div>
);
}
默認值可以根據(jù)傳入的參數(shù)進行改變。
const myAtomFamily = atomFamily({
key: ‘MyAtom’,
default: param => defaultBasedOnParam(param),
});
selectorFamily()
與Selector類似,但是可以將參數(shù)傳遞給set和get屬性。
const myNumberState = atom({
key: 'MyNumber',
default: 2,
});
const myMultipliedState = selectorFamily({
key: 'MyMultipliedNumber',
get: (multiplier) => ({get}) => {
return get(myNumberState) * multiplier;
},
// optional set
set: (multiplier) => ({set}, newValue) => {
set(myNumberState, newValue / multiplier);
},
});
function MyComponent() {
// defaults to 2
const number = useRecoilValue(myNumberState);
// defaults to 200
const multipliedNumber = useRecoilValue(myMultipliedState(100));
return <div>...</div>;
}
那么就可以通過這樣將其依賴的值傳遞進去,從而進行數(shù)據(jù)查詢。
五. 與Hox狀態(tài)管理庫相比
與hox相比: Recoi由facebook1. 來自facebook官方實驗項目, 仍然處于可觀察。2. Api較多。 hox由1. 螞蟻金服來維護的,處于相對穩(wěn)定的狀態(tài)。2. Api較少。
總結(jié):
Recoil 將應(yīng)用中的狀態(tài)抽離出來組成一個狀態(tài)樹,通過selector來與組件進行溝通。其與App中的組件呈正交性。優(yōu)點:Recoil 在后臺使用的是React本身的狀態(tài)。使用方式上完全支持hooks。未來會是一個值得期待的狀態(tài)管理框架。
參考文獻:
Recoil 文檔[4] Recoil [5] You Might Not Need Redux[6] YouTube-Recoil[7] Mobx中文文檔[8] 帶你走進Mobx的原理[9] 你需要Mobx還是Redux?[10]
參考資料
Mobx中文文檔: https://cn.mobx.js.org/
[2]https://juejin.cn/post/6844903797085437966: https://juejin.cn/post/6844903797085437966
[3]React Suspense: https://reactjs.org/docs/concurrent-mode-suspense.html
[4]Recoil 文檔: https://recoil.js.cn/docs/guides/asynchronous-data-queries
[5]Recoil : https://bytedance.feishu.cn/wiki/wikcnrGEa9YON5PqlxC7sMJSymc
[6]You Might Not Need Redux: https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367
[7]YouTube-Recoil: https://www.youtube.com/watch?v=_ISAA_Jt9kI
[8]Mobx中文文檔: https://cn.mobx.js.org/
[9]帶你走進Mobx的原理: https://juejin.cn/post/6844903797085437966#heading-6
[10]你需要Mobx還是Redux?: https://juejin.cn/post/6844903562095362056
?? 謝謝支持
喜歡的話別忘了 分享、點贊、在看 三連哦~。
點擊下方名片,關(guān)注 前端Sharing
