我為什麼要用 Recoil?
起因是最近重構了一個 props 傳遞層級非常深,元件之間狀態通訊非常頻繁的詳情頁面。整個程式碼梳理下來,很難維護,重構時考慮,做元件細分,把資料單拎出來,對資料做一個鬆散管理。
對比了下之前用過的 Redux、Mobx 感覺有點重,因為專案 純 ts + Func hooks 形式重構,應用這兩種比較常規的狀態管理工具有兩個弊端:
- 對於專案程式碼侵入比較大,要編寫大量的狀態管理程式碼(dispatch、action、reducer)
- 因為是外部庫,它們並不能訪問 React 內部的排程程式
主要還是覺得這兩個庫,對於專案使用來說,有點重了,沒必要為了一個不算很複雜的資料管理和通訊引入並不算小的 npm 依賴包。起初考慮用 Context 來處理,但是多個元件訂閱要寫多個 Provider 或者有一個根元件的 Provider 來接入資料,這就有會導致很多不必要的重繪和程式碼量。剛好有同事提了一嘴 Recoil(fb 自己為 react 提供的狀態管理工具庫),看了下資料,感覺跟自己的專案契合度還不錯,遂試用了下,效果不錯。
簡單的介紹下 Recoil
Recoil 是 FaceBook 公司提出的狀態管理方案(個人感覺像是給 react Func Comps 量身打造的輕量級狀態管理工具)。
我們都知道 React 強調的是 immutable,而 Recoil 強調的同樣也是 immutable,immuteable 給帶來的好處就是增強元件整體的應用效能。對於 immutable 不是很清楚的,建議查閱相關文件,這裡不做闡述。
Recoil 採用分散管理原子狀態的設計模式.
Recoil 提出了一個新的管理狀態單位 Atom (原子化),它是可更新和訂閱的,當一個 Atom 更新之後,每個訂閱它的元件都會與之更新重新渲染,如果多個元件使用同一個 Atom ,那麼這些元件將會共享他們的狀態。
從上圖不難看出,它的核心理念是原子化拆分資料,通過訂閱釋出的模型對資料進行管理。
Recoil基礎
初始化
使用Recoil的元件需要使用RecoilRoot元件包裹起來
import React from 'react';
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
定義狀態
上面我們已經提到了 Atom 的概念, Atom 是一種新的狀態,但是和傳統的 state 不同,它可以被任何元件訂閱,當一個 Atom 被更新時,每個被訂閱的元件都會用新的值來重新渲染。
export const pageInfoState = atom({
key: 'pageInfoState',
default: {}
});
其中 key 必須在 RecoilRoot 作用域內唯一。
default 定義預設值。
訂閱和更新狀態
useRecoilState:類似 useState 的一個 Hook,可以取到 atom 的值以及 setter 函式。
useSetRecoilState:只獲取 setter 函式,如果只使用了這個函式,狀態變化不會導致元件重新渲染。
useRecoilValue:只獲取狀態。
useResetRecoilState: 重置狀態。
import { nameState } from './store'
// useRecoilState
const NameInput = () => {
const [name, setName] = useRecoilState(nameState);
const onChange = (event) => {
setName(event.target.value);
};
return <>
<input type="text" value={name} onChange={onChange} />
<div>Name: {name}</div>
</>;
}
// useRecoilValue
const SomeOtherComponentWithName = () => {
const name = useRecoilValue(nameState);
return <div>{name}</div>;
}
// useSetRecoilState
const SomeOtherComponentThatSetsName = () => {
const setName = useSetRecoilState(nameState);
return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}
// useReSetRecoilState
const SomeOtherComponentThatSetsName = () => {
const ressetName = useSetRecoilState(nameState);
return <button onClick={ressetName}>Set Name</button>;
}
從上面我們可以看到我們取值和改變值是使用 hooks 的形式,對於 react 函式元件來說是非常友好的,程式碼侵入非常小。
派生狀態
selector 表示派生狀態,它使我們能夠建立依賴於其他 atom 的狀態(也可以進行一些計算操作)。它有一個強制性的 get 函式,有點類似 Vue 中的 computed 相似.
const lengthState = selector({
key: 'lengthState',
get: ({get}) => {
const text = get(nameState);
return text.length;
},
});
function NameLength() {
const length = useRecoilValue(charLengthState);
return <>Name Length: {length}</>;
}
非同步狀態
Recoil 提供了通過資料流圖將狀態和派生狀態對映到 React 元件的方法。真正強大的功能是圖中的函式也可以是非同步的。這使得我們可以在非同步 React 元件渲染函式中輕鬆使用非同步函式。使用 Recoil,你可以在選擇器的資料流圖中無縫地混合同步和非同步功能。只需從選擇器 get 回撥中返回 Promise ,而不是返回值本身。
例如下面的例子,如果使用者名稱儲存在我們需要查詢的某個資料庫中,那麼我們要做的就是返回一個 Promise 或使用一個 async 函式。如果 userID 發生更改,就會自動重新執行新查詢。結果會被快取,所以查詢將僅對每個唯一輸入執行一次(所以一定要保證 selector 純函式的特性,否則快取的結果將會和最新的值不一致)。
const userNameQuery = selector({
key: 'userName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(userNameQuery);
return <div>{userName}</div>;
}
總結
使用下來以後,感覺 Recoil 的分散式狀態管理,有點像 observable + computed 的模式。具體原始碼我還沒有去看過,找個時間看過之後,再補一個原始碼解讀。使用方式上,完全是迎合函式式元件 Hooks 的使用方式,沒有提供 Component 的使用方式。
官方出品必屬精品,官方文件所宣揚的高效能和可利用 React 內部的排程機制,包括之前承諾的併發模式,還是比較值得期待的(聽聽就好了)。主要還是看跟自己專案的契合度,合適你就用,工程和專案複雜度上來了,該穩重還是選穩重的狀態管理方案吧,反正我下一階段打算,把 Recoil 這玩意兒,在專案裡面鋪開體驗了。