作者: 磚家
轉發鏈接:https://mp.weixin.qq.com/s/cjwv25hSLBsUQ9m5De6vUg
前言
說到狀態管理器,輪子滿天飛。在 Class 時代,redux 與 mobox 幾乎占據了全部市場,幾乎沒有沒用過 redux 的同學。隨著 Hooks 的誕生,新的一批輪子應運而生,其中有代表性的有 unstated-next、constate 等等。當然無論什么輪子,要解決的問題都是一樣的:跨組件狀態共享。在解決這個核心問題的同時,需要盡可能的滿足以下幾個特性:
- TypeScript 支持
- 友好的異步支持
- 支持狀態互相依賴
- 同時支持 Class 與 Hooks 組件
- 使用簡單
Recoil 體驗
最近,facebook 官方出了一個狀態管理器解決方案 Recoil[1],我們來體驗一下。
準備工作
使用 Recoil,我們需要在項目最外層包一個 RecoilRoot ,這個和大部分狀態管理器一致,通過 context 來跨組件傳遞數據。
import React from 'react';
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
...
</RecoilRoot>
);
}
跨組件狀態共享
狀態最簡單的就是定義和使用。在 Recoil 中,通過 atom 來定義一個狀態。
const inputValueState = atom({
key: "inputValue",
default: ""
});
如上面的代碼所示,我們定義了一個 inputValue 狀態,它的默認值是空字符串。需要注意的是 key 字段,它應該是全局唯一的。這個 key 主要為了 debug 方便,持久化數據(數據恢復時的唯一標識),以及可以方便的看到全局 atoms 樹。消費狀態也比較簡單,通過 useRecoilState 來消費狀態。
import React from "react";
import { useRecoilState } from "recoil";
import { inputValue } from "../store";
const InputA = () => {
const [value, setValue] = useRecoilState(inputValueState);
return <input value={value} onChange={e => setValue(e.target.value)} />;
};
export default InputA;
是不是很簡單?Recoil 的基礎用法就是這樣的。我在這里寫了一個 demo[2],你可以體驗下。
狀態互相依賴
有些狀態需要依賴其它狀態,這時候就要用 selector 來定義這個狀態了。比如,我們需要定義一個新的狀態 filterdInputValue ,它是過濾 inputValue 中的數字后的值。
const filterdInputValue = selector({
key: "filterdInputValue",
get: ({get}) => {
// 通過 get 可以讀取其它狀態
const inputValue = get(inputValueState);
return inputValue.replace(/[0-9]/ig, "");
},
});
selector 比較簡單,就是為了實現狀態的依賴。你可以在這個 demo[3] 體驗下。
異步支持
良好的異步請求支持是狀態管理器必不可少的。Recoil 提供了一個 useRecoilValueLoadable 來處理異步請求。直接上例子:
const currentUserNameQuery = selector({
key: "CurrentUserName",
get: async () => {
const response = await queryUserInfo();
return response.name;
}
});
我們需要通過 selector 來定義異步狀態,如果 get 函數是一個 Promise,則代表該狀態為異步狀態,需要使用 useRecoilValueLoadable 來消費該狀態。
const UserName = () => {
const userNameLoadable = useRecoilValueLoadable(currentUserNameQuery);
switch (userNameLoadable.state) {
case "hasValue":
return <div>{userNameLoadable.contents}</div>;
case "loading":
return <div>Loading...</div>;
case "hasError":
throw userNameLoadable.contents;
}
};
從上面例子可以看到, useRecoilValueLoadable 返回的狀態,可以通過 state 字段讀取到異步請求的狀態。我寫了個 demo[4],你可以體驗下。

當然通過 useRecoilValueLoadable 來消費異步狀態,比較符合我們當前的習慣。但 Recoil 更推薦通過 React.Suspense 來消費異步狀態,這里就仁者見仁了,雖然 Suspense 可能是方向,但用起來是還不太習慣。
const UserName = () => {
const userName = useRecoilValue(currentUserNameQuery);
return <>{userName}</>
}
};
function MyApp() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<UserName />
</React.Suspense>
);
}
評價
優點
- 之前狀態管理器滿天飛,如果官方能一統天下,應該算一件好事情。
- 對 React concurrent 模式支持良好。
不足
當前 Recoil 還處于開發階段,文檔都還不是很全。基于現狀,說幾點我的感受。
1. 沒有使用 ts 實現,目前不支持 ts
這點我很驚訝,也是寫這個文章的時候才發現的,很奇怪。講道理 Recoil 支持 typescript 應該是順手的事情,可能后期需要來個 @types/recoil 吧。
2. 目前沒有支持 Class 組件消費狀態。
這個特性應該是必備的,應該不會徹底拋棄 Class 組件。估計下個版本肯定會支持的這個特性的。實現成本較低,不支持的話就太反人類了。
3. API 偏多,有一定上手成本。

各類 API 一共有 19 個,偏復雜了。感覺很多都是可以合并的,比如 atom 和 selector 合并成一個等等(也可能是我考慮不成熟)。建議官方可以考慮精簡精簡,本來是一個很簡單的東西,搞的太復雜了。
4. 消費較繁瑣
我們需要消費一個狀態的時候,需要 import 兩個東西,比較繁瑣。
import { useRecoilState } from "recoil";
import { inputValueState } from "../store";
// 用法
useRecoilState(inputValueState);
本來應該可以直接通過字符串 key 消費的,但這樣和 redux 問題一樣了,無法支持 ts。
import { useRecoilState } from "recoil";
useRecoilState('inputValueState');
無論如果,import 兩個東西不是一個好的用法。
5. 沒有足夠的亮點
沒有看到讓人眼前一亮的東西,沒有使用沖動。靜觀發展~
后記
Recoil 整體看下來,比較中庸,需要靜觀發展。另外推薦一下我目前正在用的最簡單的 React 狀態管理器 hox[5],只有一個 API,非常符合直覺,沒有任何上手成本,完全擁抱 Hooks 。