Recoil 狀態管理方案的淺入淺出
本文作者:江水
背景: Recoil
是 Facebook
推出的一款專門針對React
應用的狀態管理庫,在一定程度上代表了目前的一種發展趨勢,在使用時覺得一些理念很先進,能極大地滿足作為一個前端開發者的資料需求,本文對 Recoil
的這些特性做一個梳理。
根據官網的介紹,Recoil
的資料定義了一個有向圖 (directed graph),狀態的變更是通過改變圖的根節點 (atom),再通過純函式 (selector) 流向 React
元件。
同時 Recoil
的狀態定義是增量和分散式的,增量意味著我們可以在用的時候再定義新的狀態,而不必將所有狀態提前定義好再消費。分散式意味著狀態的定義可以放在任何位置,不必統一註冊到一個檔案中。這樣的好處是一方面可以簡化狀態的定義過程,另一方面也可以很好地應用在 code-splitting 場景。
在一個應用中開啟 Recoil
非常簡單,只需要包裹一個 RecoilRoot
即可。
```js import { RecoilRoot } from 'recoil';
ReactDOM.render(
狀態定義,原子和選擇器
Recoil
允許使用 atom
和 selector
兩個函式定義基礎和推導狀態。
atom
基本用法,這裡定義了相關的原子屬性,需要使用唯一 key
來描述這個 atom
。 Recoil
中不允許重複的 key 出現,包括後面提到的 selector
。
```js const firstNameAtom = atom({ key: 'first name atom', default: '' });
const lastNameAtom = atom({ key: 'last name atom', default: '' }); ```
使用時通過 useRecoilState
這個 hooks 獲取狀態,可以看到它和 useState
很像,所以可以很輕鬆地將傳統的React狀態遷移到 Recoil
中。
```diff function UserProfile() { - const [firstName, setFirstName] = useState(''); + const [firstName, setFirstName] = useRecoilState(firstNameAtom);
return (
很多時候我們只想獲取資料而不想修改,或者反之,此時可以用語法糖 useRecoilValue
和 useSetRecoilState
```js function UserProfile() { const firstName = useRecoilValue(firstNameAtom);
return (
Recoil
會根據哪裡用到了這些狀態自動建立一種依賴關係,當發生變更時 Recoil
只會通知對應的元件進行更新。
selector
的用法和 atom
很像,構造一個 selector
至少需要一個唯一的 key
和 get
函式。
js
const nameSelector({
key: 'my name selector',
get: ({ get }) => {
return get(firstNameAtom) + ' ' + get(lastNameAtom);
}
});
在 selector
中可以讀寫任意 atom
/ selector
,沒有任何限制。只有 get
方法的 selector
是隻讀的,如果需要可寫,也支援傳入 set
方法。
js
const nameSelector({
key: 'my name selector',
get: ({ get }) => {
return get(firstNameAtom) + ' ' + get(lastNameAtom);
},
set: ({ get, set }, value) => {
const names = value.split(' ');
set(firstNameAtom, names?.[0]);
set(lastNameAtom, names?.[1]);
}
});
值得一提的是,selector支援從網路非同步獲取資料,這裡才是有趣的開始,也是和其他狀態管理的最大的不同,Recoil的狀態不僅是純狀態,也可以是來自網路的狀態。
js
const userSelector = selector({
name: 'user selector',
get: () => {
return fetch('/api/user');
}
});
使用 selector
時和 atom
一樣可以通過 useRecoilState
, useRecoilValue
, useSetRecoilState
這幾個 hook。
```js function App() { const user = useRecoilValue(userSelector);
...
} ```
這樣的特性使得我們的程式碼很容易重構,假如一開始一個屬性是一個 atom
, 後面希望變成一個計算屬性,此時可以很輕鬆地替換這部分邏輯,而無需修改業務層程式碼。
Recoil
還可以更強大,用下面一張圖可以大致概括下,其完全可以當成一個統一的資料抽象層,將後端資料通過 http, ws, GraphQL 等技術對映到前端元件中。
atomFamily selectorFamily 批量建立狀態的解決方案
在一些場景中會有需要批量建立狀態的情況,我們會例項化多個相同的元件,每個元件都需要對應一個自己獨立的狀態元素,此時就可以使用 xxxFamily
api。
```js const nodeAtom = atomFamily({ key: 'node atom', default: {} });
function Node({ nodeId }) { const [node, setNode] = useRecoilState(nodeAtom(nodeId)); } ```
可以看到,atomFamily
返回的是一個函式,而不是一個 RecoilState
物件。傳入不同的 nodeId
會檢查是否之前已存在,如果存在則複用之前的,不存在則建立並使用預設值初始化。
同理,對於 selectorFamily
。
``js
const userSelector = selectorFamily({
key: 'user selector family',
get: (userId) => () => {
return fetch(
/api/user/${userId}`);
}
});
function UserDetail({ userId }) { const user = useRecoilValue(userSelector(userId)); } ```
由於批量建立可能會導致記憶體洩漏,所以 Recoil
也提供了快取策略管理,分別為 lru
, keep-all
, most-recent
,可以根據實際需要選取。
Suspense 與 Hooks
上文提到每個 atom
, selector
背後可以是本地資料,也可以是網路狀態(對,沒錯, atom
也可以是個非同步資料,常用的如 atom
初始化是個非同步,後續變成同步資料),在元件消費時無需關心背後的實際來源,使用遠端資料就像使用本地資料一樣輕鬆。
來看一個普通的獲取資料並展示元件的例子。
```js function getUser() { return fetch('/api/user'); }
function LocalUserStatus() { const [loading, setLoading] = useState(false); const [user, setUser] = useState(null);
useEffect(() => { setLoading(true); getUser().then((user) => { setUser(user); setLoading(false); }) }, []);
if (loading) { return null; }
return (
對於這種開發習慣 (往往被稱為 Fetch-on-Render):我們需要一個 useEffect
來獲取資料,再需要設定一些 loading
, error
狀態處理邊界狀態,如果這個資料不是一個放在全域性且處在頂層的資料,而是散落在子元件中消費,則每一個使用的地方都要執行類似的邏輯。
下面看下 Recoil
的寫法
```js
const localUserAtom = atom({
key: 'local user status',
default: selector({ // <-------- 預設值來自 selector
key: 'user selector',
get: () => {
return fetch('/api/user');
}
})
});
function LocalUserStatus() { const localUser = useRecoilValue(localUserAtom);
return (
<div>
{ localUser.name }
</div>
)
} ```
這裡在元件層是不關心資料從哪來的, Recoil
會自動按需請求資料。
相比之下,後者的程式碼就簡潔許多(Render-as-You-Fetch),而且背後並沒有發明新的概念,用到的都是 React
原生的特性,這個特性就是 Suspense
。
如果使用了一個非同步的 atom
或 selector
,則外層需要一個 Suspense
處理網路未返回時的 loading
狀態。也可以套一層 ReactErrorBoundary
處理網路異常的情況。
```js // UserProfile 中使用了一個需要從網路中載入的資料 function LocalUserStatus() { const user = useRecoilValue(localUserAtom);
... }
function App() { return (
<Suspense fallback={<Loading />}>
<LocalUserStatus />
</Suspense>
<div>底部</div>
</div>
); } ```
通過把通用的 Loading
和 Error
邏輯剝離出去,使得一般元件內的條件分支減少 66%,首次渲染即是資料準備完成的狀態,減少了額外的處理邏輯以及 hooks 過早初始化問題。
hooks 過早初始化問題可參考拙文: Recoil 這個狀態管理庫,用起來可能是最爽的
useRecoilValueLoadable(state) 讀取資料,但返回的是個Loadable
和 useRecoilValue
不同,useRecoilValueLoadable
不需要外層 Suspense
,相當於將邊界情況交給使用者處理。
Loadable
的物件結構如下:
它的作用就是我們能夠獲取到當前資料是 loading
, 還是已經 hasValue
, 手動處理這些狀態,適合靈活處理頁面渲染的場景。
```js const userLoadable = useRecoilValueLoadable(userSelector);
const isLoading = userLoadable.state === 'loading'; const isError = userLoadable.state === 'hasError'; const value = userLoadable.getValue(); ```
Recoil 用來對映外部系統
在一些場景下我們希望 Recoil
能夠和外部系統進行同步,典型的例子例如 react-router
的 history
同步到 atom
中,原生 js 動畫庫狀態和 Recoil
同步,將 atom
和遠端 mongodb
同步。通過直接讀寫 atom
就能直接讀寫外部系統,開發效率可以大大提高。
這種場景下可以藉助 recoil-sync
這個包,下面列舉兩個案例。
使用 sharedb
+ recoil-sync
可以讓 atom
和 mongodb
/postgres
等資料庫進行狀態同步,從而讓遠端資料庫修改如同本地修改一樣方便。
js
// 對其的修改會實時同步到遠端mongodb中
const [name, setName] = useRecoilState(nameAtom);
使用 recoil-sync
將 atom
和 pixi.js
動畫元素進行狀態同步
http://codesandbox.io/s/nice-swirles-dmdlq0?file=/src/animation-canvas.js
此時可以將畫布上的一些精靈變成受控模式。
由於同步過程中會產生資料格式校驗問題, recoil-sync
使用 @recoiljs/refine
用來提供資料校驗和不同版本資料遷移功能。
Recoil 狀態快照
由於狀態粒度較細,對於需要批量設定 RecoilState
的場景, Recoil
有 Snapshot
的概念,適合 ssr
時注入首屏資料,建立快照進行回滾,批量更新等場景。
填充 SSR 的資料
```js function initState(snapshot) { snapshot.set(atoms.userAtom, { name: 'foo', }); snapshot.set(atoms.countAtom, 0); }
export default function App() {
return (
應用資料回滾
```js function TimeMachine() { const snapshotRef = useRef(null); const [count, setCount] = useRecoilState(countAtom);
const onSave = useRecoilCallback( ({ snapshot }) => () => { snapshot.retain(); snapshotRef.current = snapshot; }, [] );
const onRevoca = useRecoilCallback( ({ gotoSnapshot }) => () => { if (snapshotRef.current) { gotoSnapshot(snapshotRef.current); } }, [] );
return (
不使用 async-await也能實現非同步轉同步程式碼
在 React
的世界裡一直存在著一種很奇怪的程式碼技巧,這種技巧能夠不利用 generator
或者 async
就能達到非同步轉同步的功能,在瞭解 Recoil
的一些用法時我也留意到這種現象,很有意思,這裡介紹下:
假如 userSelector
是一個需要從網路中獲取的狀態,對其的讀取可視作一個非同步操作,但是在寫 selector
時我們可以以一種同步的方式來寫。
const userNameSeletor = selector({
key: 'user name selector',
get: ({ get }) => {
const user = get(userSelector); <--- 這裡背後是個網路請求
return user.name;
}
});
這種寫法之前出現過,在元件中使用 selector
時我們也沒有考慮其非同步性。
js
function UserProfile() {
const user = useRecoilValue(userProfile); <---- 這裡背後也是個網路請求
const userId = user.id;
return <div>uid: {userId}</div>;
}
在元件中使用時是利用了外層的 Suspense
執行,在上述的 get
回撥中內部也隱式地使用了相似手段,當發生非同步時 get
方法會將Promise
當成異常丟擲,當非同步結束時再重新執行這個函式,所以這個函式本身會執行兩次,有點黑魔法的感覺,這也同樣要求我們在此時應該保證get
是一個純函式。如果一個 selector
的 get
回撥中存在網路請求,那就不再是一個純函式,此時需要保證:網路請求是在所有非同步selector執行之後呼叫。
```js // 正確的用法 const nameSelector = selector({ key: "name selector", get: async ({ get }) => { get(async1Selector); get(async2Selector); await new Promise((resolve) => { setTimeout(resolve, 0); }); return 1; } });
// 錯誤的用法 const nameSelector = selector({ key: "name selector", get: async ({ get }) => { get(async1Selector); await new Promise((resolve) => { setTimeout(resolve, 0); }); get(async2Selector); return 1; } }); ```
最後,關於程式碼直覺,心智負擔
最近很多人會討論一個庫是否適合引入時會說到這兩個詞,在對一個庫不瞭解的情況下我們很容易就說出“這個庫太複雜了”,“要記憶的api太多了” 這類的話。在 Recoil
的世界裡如果我們接受了 atom
, selector
,那麼 atomFamily
, selectorFamily
也很容易理解。由於已經習慣了 useState
那麼 useRecoilValue
, useSetRecoilValue
也很容易接受, 都很符合 hooks 的直覺。
Recoil
的 api 和 react
自身的 useState
, useCallback
, Suspense
是概念一致的, 二者的使用反而會加深對 react
框架本身的理解,一脈相承,沒有引入其他的程式設計概念,api雖多但心智負擔並不大。舉個反例,如果在 react
中使用 observable
型別的狀態管理,我可能會思考 useEffect
在一些場景是否能夠按預期工作,雖然某些特性使用起來很舒服,但卻加深了心智負擔。
如果有誤還望指正。
本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!
- 社交場景下iOS訊息流互動層實踐
- 你構建的程式碼為什麼這麼大
- 扒一扒 Jetpack Compose 實現原理
- Recoil 狀態管理方案的淺入淺出
- 基於自建 VTree 的全鏈路埋點方案
- 雲音樂 iOS 啟動效能優化「開荒篇」
- 雲音樂播放頁直播推薦實戰
- 雲音樂iOS端網路圖片下載優化實踐
- 雲音樂 iOS 啟動效能優化「開荒篇」
- 基於 React Native 的動態列表方案探索
- RTC 腳手架的設計和實現
- 專案RTL語言適配實踐中遇到的問題和總結
- Swift 中的 JSON 反序列化
- 網易雲音樂機器學習平臺實踐
- React Native中實現動態匯入
- 雲音樂FeatureStore建設與實踐
- 雲音樂預估系統建設與實踐
- 直播活動系統:基於訊息匯流排的組合能力
- systrace 統計方法耗時
- NUMA架構下的預估系統性能優化