在 React Europe 2020 Conference
上, Facebook
軟體工程師 Dave McCabe
介紹了一個新的狀態管理庫 Recoil
。
Recoil
現在還處於實驗階段,現在已經在 Facebook
一些內部產品中用於生產環境。畢竟是官方推出的狀態管理框架,之前沒時間仔細研究,藉著國慶期間看了看,給大家分享一下。
State 和 Context 的問題
假設我們有下面一個場景:有 List
和 Canvas
兩個元件,List 中一個節點更新後,Canvas 中的節點也對應更新。
最常規則做法是將一個 state
通過父元件分發給 List
和 Canvas
兩個元件,顯然這樣的話每次 state
改變後 所有節點都會全量更新。
當然,我們還可以使用 Context API
,我們將節點的狀態存在一個 Context
內,只要 Provider
中的 props
發生改變, Provider
的所有後代使用者都會重新渲染。
為了避免全量渲染的問題,我們可以把每個子節點儲存在單獨的 Context
中,這樣每多一個節點就要增加一層 Provider
。
但是,如果子節點是動態增加的呢?我們還需要去動態增加 Provider
,這會讓整個樹再次重新渲染,顯然也是不符合預期的。
引入 Recoil
Recoil
本身就是為了解決 React
全域性資料流管理的問題,採用分散管理原子狀態的設計模式。
Recoil
提出了一個新的狀態管理單位 Atom
,它是可更新和可訂閱的,當一個 Atom
被更新時,每個被訂閱的元件都會用新的值來重新渲染。如果從多個元件中使用同一個 Atom
,所有這些元件都會共享它們的狀態。
你可以把 Atom
想象為為一組 state
的集合,改變一個 Atom
只會渲染特定的子元件,並不會讓整個父元件重新渲染。
用 Redux 或 Mobx 不可以嗎?
因為 React
本身提供的 state
狀態在跨元件狀態共享上非常苦難,所以我們在開發時一般藉助一些其他的庫如 Redux、Mobx
來幫助我們管理狀態。這些庫目前正被廣泛使用,我們也並沒有遇到什麼大問題,那麼 Facebook
為什麼還要推出一款新的狀態管理框架呢?
使用 Redux、Mobx
當然可以,並沒有什麼問題,主要原因是它們本身並不是 React
庫,我們是藉助這些庫的能力來實現狀態管理。像 Redux
它本身雖然提供了強大的狀態管理能力,但是使用的成本非常高,你還需要編寫大量冗長的程式碼,另外像非同步處理或快取計算也不是這些庫本身的能力,甚至需要藉助其他的外部庫。
並且,它們並不能訪問 React
內部的排程程式,而 Recoil
在後臺使用 React
本身的狀態,在未來還能提供併發模式這樣的能力。
基礎使用
初始化
使用 recoil
狀態的元件需要使用 RecoilRoot
包裹起來:
import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
useSetRecoilState
} from 'recoil';
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
定義狀態
上面我們已經提到了 Atom
的概念, Atom
是一種新的狀態,但是和傳統的 state
不同,它可以被任何元件訂閱,當一個 Atom
被更新時,每個被訂閱的元件都會用新的值來重新渲染。
首先我們來定義一個 Atom
:
export const nameState = atom({
key: 'nameState',
default: 'ConardLi'
});
這種方式意味著你不需要像 Redux
那樣集中定義狀態,可以像 Mobx
一樣將資料分散定義在任何地方。
要建立一個 Atom
,必須要提供一個 key
,其必須在 RecoilRoot
作用域中是唯一的,並且要提供一個預設值,預設值可以是一個靜態值、函式甚至可以是一個非同步函式。
訂閱和更新狀態
Recoil
採用 Hooks
方式訂閱和更新狀態,常用的是下面三個 API:
useRecoilState
:類似 useState 的一個Hook
,可以取到atom
的值以及setter
函useSetRecoilState
:只獲取setter
函式,如果只使用了這個函式,狀態變化不會導致元件重新渲染useRecoilValue
:只獲取狀態
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>;
}
派生狀態
selector
表示一段派生狀態,它使我們能夠建立依賴於其他 atom
的狀態。它有一個強制性的 get
函式,其作用與 redux
的 reselect
或 MobX
的 @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}</>;
}
selector 是一個純函式:對於給定的一組輸入,它們應始終產生相同的結果(至少在應用程式的生命週期內)。這一點很重要,因為選擇器可能會執行一次或多次,可能會重新啟動並可能會被快取。
非同步狀態
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
推薦使用 Suspense
,Suspense
將會捕獲所有非同步狀態,另外配合 ErrorBoundary
來進行錯誤捕獲:
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
總結
Recoil
推崇的是分散式的狀態管理,這個模式很類似於 Mobx
,使用起來也感覺有點像 observable + computed
的模式,但是其 API 以及核心思想設計的又沒有 Mobx
一樣簡潔易懂,反而有點複雜,對於新手上手起來會有一定成本。
在使用方式上完全擁抱了函式式的 Hooks
使用方式,並沒有提供 Componnent
的使用方式,目前使用原生的 Hooks API
我們也能實現狀態管理,我們也可以使用 useMemo
創造出派生狀態,Recoil
的 useRecoilState
以及 selector
也比較像是對 useContext、useMemo
的封裝。
但是畢竟是 Facebook
官方推出的狀態管理框架,其主打的是高效能以及可以利用 React
內部的排程機制,包括其承諾即將會支援的併發模式,這一點還是非常值得期待的。
另外,其本身的分散管理原子狀態的模式、讀寫分離、按需渲染、派生快取等思想還是非常值得一學的。
最後
文章中如有錯誤,歡迎在評論區指正;如果文章對你有幫助,歡迎點贊、評論、分享、希望能幫到更多人。
本文首發於公眾號《code祕密花園》歡迎大家關注,原文:Facebook 新一代 React 狀態管理庫 Recoil