Recoil について勉強した
Fecebook が新しく発表した Recoil について
自分の学習手順
- Getting Started | Recoil を写経して動かす
- Facebook 製の新しいステート管理ライブラリ「Recoil」を最速で理解する - uhyo/blog で非同期周りを理解
- 公式ドキュメントの API Reference で理解 <RecoilRoot ...props /> | Recoil
これは自分が写経しながら書いた型定義。色々足りてないがチュートリアルで出る範囲は理解できる。
declare module "recoil" {
export type RecoilState<T> = {};
export const RecoilRoot: React.ComponentType<{
initializeState?: (options: {
set: <T>(recoilVal: RecoilState<T>, newVal: T) => void;
setUnvalidatedAtomValues: (atomMap: Map<string, unknown>) => void;
dangerouslyAllowMutability?: boolean;
}) => void;
children: any;
}>;
export function atom<T>(input: {
key: string;
default: ValueType;
}): RecoilState<T>;
export function selector<T>(input: {
key: string;
get(helpers: {
get<U>(atom: RecoilState<U>): U;
getPromise<U>(atom: RecoilState<U>): Promise<U>;
}): T;
set?(
helpers: {
set<U>(atom: RecoilState<U>, newVal: U): void;
},
newVal: T
): void;
});
export function useRecoilValue<T>(atom: RecoilState<T>): T;
export function useRecoilState<T>(
atom: RecoilState<T>
): [T, (action: React.SetStateAction<T>) => void];
export function useSetRecoilState<T>(
atom: RecoilState<T>
): (action: React.SetStateAction<T>);
}
DefinitelyTyped に PR が出てるが、まだマージされてない。
Add type definitions for recoil by csantos42 · Pull Request #44756 · DefinitelyTyped/DefinitelyTyped
後述する waitForAll
などのユーティリティが書きかけ。
思想的な部分
- Redux は常に一つでかつすべての状態ありきの思想なので、State とその手続きが宣言される。このせいで、常に使わない State も初期化しないといけない
- Recoil は状態を依存グラフで表現する。atom とそれを参照する selector があり、selector が atom を購読して反映される
また、 selector への set で atom を同期/非同期に書き換えるというインターフェースになっている。実体は atom を実体とした単方向サブスクリプションだが、コード上は双方向にもなりうる。
selector が mutable
atom と selector も、 const [state, setState] = useRecoilState(atom_or_selector)
できる。 selector が setState できる、とはどううことだろうか。
単一な状態を持つ atom だけではなく、グラフ中で selector ノードも、まるで Mutable かのような API を持つ。自分自身への更新時、非同期に個別の atom への set を再発行できるファサードになっている。
公式サンプルからの引用だが、次のコードは華氏と摂氏の二値が連動して動く。
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from "recoil";
const tempFahrenheit = atom<number>({
key: "tempFahrenheit",
default: 32,
});
const tempCelcius = selector<number>({
key: "tempCelcius",
get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});
function TempCelcius() {
const [tempF, setTempF] = useRecoilState<number>(tempFahrenheit);
const [tempC, setTempC] = useRecoilState<number>(tempCelcius);
const addTenCelcius = () => setTempC(tempC + 10);
const addTenFahrenheit = () => setTempF(tempF + 10);
return (
<div>
Temp (Celcius): {tempC}
<br />
Temp (Fahrenheit): {tempF}
<br />
<button onClick={addTenCelcius}>Add 10 Celcius</button>
<br />
<button onClick={addTenFahrenheit}>Add 10 Fahrenheit</button>
</div>
);
}
元となる状態自体は tempFahrenheit が atom で、その selector としての tempCelcius だが、tempCelcius への set で、 tempFahrenheit を書き換えて、値を反映している。
これは正直、議論が分かれそうな設計だと思っていて、 vue の computed property などに近いようにみえて、computed property には不可能な副作用も記述できるが、その実装が正しいかどうかは、実装者が責任を持つことになるだろう。
単なる set では atom を直接書き換えたほうがきれいだと思うが、ここで面白いのは、set の実装が非同期の Promise を取れるということだ。実質 redux middleware で多段 dispatch するときと同じようなコードになる。
非同期な state と Suspense
ここで state / selector は非同期を取れるので、 get / set は async/await のインターフェースをとることができる。
1 秒後に値を表示する例
const lazyState = selector({
key: "lazyState",
get: async () => {
await new Promise((r) => setTimeout(r, 1000));
return 1;
},
});
function LazyValue() {
const value = useRecoilValue<number>(lazyState);
return <div>{value}</div>;
}
function App() {
return (
<RecoilRoot>
<Suspense fallback="loading">
<LazyValue />
</Suspense>
</RecoilRoot>
);
}
ReactDOM.render(<App />, document.querySelector("main"));
実装をみると、最初の useRecoilValue
で throw new Promise(...)
を発行し、 Suspense にキャッチさせて解決させるやつ。
これを使うと、ネットワーク越しのリソースを抽象したりすることができそう。
Redux との比較
- 大域の再計算にならないので、React Component から参照されるときの再計算が、最小限
- 必要なコードだけビルドに含めることができる
- 状態更新の手続きは reducer ではなく、setState の React.SetStateAction 準拠
- 非同期抽象が middleware ではなく、 promise + suspense になる
自分がまだわかってないところ
Redux では常にひとつの状態が全部の状態を表すので、SSR で渡したり、 localStorage に状態を書き込んでから、再訪時に状態を復元する、というのが容易だった。Recoil では、RecoilRoot がすべての状態を管理しているはずだが、それを吐き出したり、よみこんだりする方法が(まだ)ない。
今ちょうどリロードしたらドキュメントに Core 以外の Utils というのが生えて、この辺の waitForAll
にその機能がありそうなので、しばらく待ったほうがよさそう。
https://recoiljs.org/docs/api-reference/utils/waitForAll
可能なら React に依存せず、Recoil のリソースの依存グラフだけで実行できると、サーバー上で hydration のために初期実行できて、嬉しい気がする。
で、結局使い物になるの?
- 自分的にはアリ。ただし、selector への set は、非同期のユースケースを限定したほうが良さそう
- 状態をダンプする系の API は足りてない。