Skip to content

Commit 4b7592a

Browse files
committed
Recoil
1 parent 0651e38 commit 4b7592a

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

docs/study-recoil.mdx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: Recoil について勉強した
3+
created: 1589622364679
4+
---
5+
6+
Fecebook が新しく発表した [Recoil](https://recoiljs.org/) について
7+
8+
## 自分の学習手順
9+
10+
- [Getting Started \| Recoil](https://recoiljs.org/docs/introduction/getting-started/) を写経して動かす
11+
- [Facebook 製の新しいステート管理ライブラリ「Recoil」を最速で理解する \- uhyo/blog](https://blog.uhy.ooo/entry/2020-05-16/recoil-first-impression/) で非同期周りを理解
12+
- 公式ドキュメントの API Reference で理解 [<RecoilRoot \.\.\.props /> \| Recoil](https://recoiljs.org/docs/api-reference/core/RecoilRoot)
13+
14+
これは自分が写経しながら書いた型定義。色々足りてないがチュートリアルで出る範囲は理解できる。
15+
16+
```ts
17+
declare module "recoil" {
18+
export type RecoilState<T> = {};
19+
export const RecoilRoot: React.ComponentType<{
20+
initializeState?: (options: {
21+
set: <T>(recoilVal: RecoilState<T>, newVal: T) => void;
22+
setUnvalidatedAtomValues: (atomMap: Map<string, unknown>) => void;
23+
dangerouslyAllowMutability?: boolean;
24+
}) => void;
25+
children: any;
26+
}>;
27+
export function atom<T>(input: {
28+
key: string;
29+
default: ValueType;
30+
}): RecoilState<T>;
31+
export function selector<T>(input: {
32+
key: string;
33+
get(helpers: {
34+
get<U>(atom: RecoilState<U>): U;
35+
getPromise<U>(atom: RecoilState<U>): Promise<U>;
36+
}): T;
37+
set?(
38+
helpers: {
39+
set<U>(atom: RecoilState<U>, newVal: U): void;
40+
},
41+
newVal: T
42+
): void;
43+
});
44+
export function useRecoilValue<T>(atom: RecoilState<T>): T;
45+
export function useRecoilState<T>(
46+
atom: RecoilState<T>
47+
): [T, (action: React.SetStateAction<T>) => void];
48+
export function useSetRecoilState<T>(
49+
atom: RecoilState<T>
50+
): (action: React.SetStateAction<T>);
51+
}
52+
```
53+
54+
DefinitelyTyped に PR が出てるが、まだマージされてない。
55+
56+
[Add type definitions for recoil by csantos42 · Pull Request \#44756 · DefinitelyTyped/DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/44756)
57+
58+
後述する `waitForAll` などのユーティリティが書きかけ。
59+
60+
## 思想的な部分
61+
62+
- Redux は常に一つでかつすべての状態ありきの思想なので、State とその手続きが宣言される。このせいで、常に使わない State も初期化しないといけない
63+
- Recoil は状態を依存グラフで表現する。atom とそれを参照する selector があり、selector が atom を購読して反映される
64+
65+
また、 selector への set で atom を非同期に書き換えるというインターフェースになっている。単方向サブスクリプションではなく、双方向。
66+
67+
## selector が immutable とはどういうことか。
68+
69+
単一な状態を持つ atom だけではなく、グラフ中で selector ノードも、まるで Mutable かのような API を持つ。自分自身への更新時、非同期に個別の atom への set を再発行できるファサードになっている。
70+
71+
公式サンプルからの引用だが、次のコードは華氏と摂氏の二値が連動して動く。
72+
73+
```tsx
74+
import {
75+
RecoilRoot,
76+
atom,
77+
selector,
78+
useRecoilState,
79+
useRecoilValue,
80+
} from "recoil";
81+
82+
const tempFahrenheit = atom<number>({
83+
key: "tempFahrenheit",
84+
default: 32,
85+
});
86+
87+
const tempCelcius = selector<number>({
88+
key: "tempCelcius",
89+
get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
90+
set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
91+
});
92+
93+
function TempCelcius() {
94+
const [tempF, setTempF] = useRecoilState<number>(tempFahrenheit);
95+
const [tempC, setTempC] = useRecoilState<number>(tempCelcius);
96+
97+
const addTenCelcius = () => setTempC(tempC + 10);
98+
const addTenFahrenheit = () => setTempF(tempF + 10);
99+
100+
return (
101+
<div>
102+
Temp (Celcius): {tempC}
103+
<br />
104+
Temp (Fahrenheit): {tempF}
105+
<br />
106+
<button onClick={addTenCelcius}>Add 10 Celcius</button>
107+
<br />
108+
<button onClick={addTenFahrenheit}>Add 10 Fahrenheit</button>
109+
</div>
110+
);
111+
}
112+
```
113+
114+
元となる状態自体は tempFahrenheit が atom で、その selector としての tempCelcius だが、tempCelcius への set で、 tempFahrenheit を書き換えて、値を反映している。
115+
116+
これは正直、議論が分かれそうな設計だと思っていて、 vue の computed property などに近いようにみえて、computed property には不可能な副作用も記述できるが、その実装が正しいかどうかは、実装者が責任を持つことになるだろう。
117+
118+
単なる set では atom を直接書き換えたほうがきれいだと思うが、ここで面白いのは、set の実装が非同期の Promise を取れるということだ。
119+
120+
## 非同期な state と Suspense
121+
122+
ここで state / selector は非同期を取れるので、 get / set は async/await のインターフェースをとることができる。
123+
124+
1 秒後に値を表示する例
125+
126+
```tsx
127+
const lazyState = selector({
128+
key: "lazyState",
129+
get: async () => {
130+
await new Promise((r) => setTimeout(r, 1000));
131+
return 1;
132+
},
133+
});
134+
135+
function LazyValue() {
136+
const value = useRecoilValue<number>(lazyState);
137+
return <div>{value}</div>;
138+
}
139+
140+
function App() {
141+
return (
142+
<RecoilRoot>
143+
<Suspense fallback="loading">
144+
<LazyValue />
145+
</Suspense>
146+
</RecoilRoot>
147+
);
148+
}
149+
150+
ReactDOM.render(<App />, document.querySelector("main"));
151+
```
152+
153+
実装をみると、最初の `useRecoilValue``throw new Promise(...)` を発行し、 Suspense にキャッチさせて解決させるやつ。
154+
155+
これを使うと、ネットワーク越しのリソースを抽象したりすることができそう。
156+
157+
## Redux との比較
158+
159+
- 大域の再計算にならないので、React Component から参照されるときの再計算が、最小限
160+
- 必要なコードだけビルドに含めることができる
161+
- 状態更新の手続きは reducer ではなく、setState の React.SetStateAction 準拠
162+
- 非同期抽象が middleware ではなく、 promise + suspense になる
163+
164+
## 自分がまだわかってないところ
165+
166+
Redux では常にひとつの状態が全部の状態を表すので、SSR で渡したり、 localStorage に状態を書き込んでから、再訪時に状態を復元する、というのが容易だった。Recoil では、RecoilRoot がすべての状態を管理しているはずだが、それを吐き出したり、よみこんだりする方法が(まだ)ない。
167+
168+
今ちょうどリロードしたらドキュメントに Core 以外の Utils というのが生えて、この辺の `waitForAll` にその機能がありそうなので、しばらく待ったほうがよさそう。
169+
170+
https://recoiljs.org/docs/api-reference/utils/waitForAll
171+
172+
可能なら React に依存せず、Recoil のリソースの依存グラフだけで実行できると、サーバー上で hydration のために初期実行できて、嬉しい気がする。
173+
174+
## で、結局使い物になるの?
175+
176+
- 自分的にはアリ。ただし、selector への set は、非同期のユースケースを限定したほうが良さそう
177+
- 状態をダンプする系の API は足りてない。

0 commit comments

Comments
 (0)