atomRewind
https://gyazo.com/3431b8078f4d4ff4ef474fc24a31964a
Atom の reset の仕様を「ハードリセット」にしたオリジナルの Atom
こういう機能が公式にある訳ではない(緑色の reset の矢印は本来存在しない)。
code:sample.tsx
const serverDataAtom = atomRewind({
key: 'serverDataAtom',
default: async () => {
const response = await fetch('/getData');
return await response.json();
}
});
function Component() {
const reset = useResetRecoilState(serverDataAtom);
return (
<button onClick={reset}>Fetch again</button>
);
}
これは atomRewind の使用例
もしもこれが atom ならば、fetch されるのは初回の一度だけ
reset は最初に実行した値をセットする操作だから
モチベーション
サーバ側でデータの変更があった時に再度取得するにはどうすれば良いか?
Atom には一度 hasValue になった AtomValue を再び pending にする方法は提供されていない
非同期処理を Component に書いて処理結果を Atom に set することはできるが、取得ロジックが RecoilState と Component に分散してしまい、あまり面白くない
code:notgood.tsx
async function task() { ... }
const serverDataAtom = atom({
key: 'serverDataAtom',
default: task() // 非同期処理スタート
});
function Component() {
const set = useSetRecoilState(serverDataAtom);
const reset = useCallback(() => task().then(set)); // 非同期処理スタート
return (
<button onClick={reset}>Fetch again</button>
);
}
面白くないというか、 RecoilValue が hasValue のままなので、非同期処理が完了するまで古い値が残って困る
この例では省略しているが、現実的には useCallback(...) の最初にロード中であることを表すフラグなどの State を設定する必要があるだろう
しかし、他のコンポーネントからも RecoilValue が参照される場合は、ロード中であることを表すフラグ自体も Atom で宣言する必要がある
2つの RecoilState を1つのデータとして振舞うように設計することは Recoil ではアンチパターンだと思う 複数の RecoilState の値を同時に(=アトミックに)変更するには、Snapshot を使う必要があるため 2つの RecoilState を購読する3つ目の ReadWriteSelector を新たに作るならアリ?面倒すぎるが。
例外処理もイマイチ。初期化時の非同期エラーは hasError として Loadable 保持されるのに、reset された時の非同期エラーは useCallback(...) の中で catch しなければいけない
そのエラーも、他のコンポーネントから参照される可能性がある場合には、新たに Atom として宣言する必要がある
実装
code:atomRewind.ts
import { atom, DefaultValue, selector } from 'recoil';
export interface AtomRewindOptions<T> {
key: string;
default: () => Promise<T>;
dangerouslyAllowMutability?: boolean;
}
/**
* reset() することで options.default を再実行する Atom
*/
export function atomRewind<T>(options: AtomRewindOptions<T>) {
// reset または set された回数
const setCount = atom({
key: ${options.key}__setCount,
default: 0
});
// set された場合は値を保持
// reset された場合は reset されたことを保持
let overrideValue: T | DefaultValue = new DefaultValue();
return selector<T>({
key: options.key,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
get: ({ get }) => {
get(setCount); // set と reset を購読
return overrideValue instanceof DefaultValue
? options.default() // reset されたので関数を再実行
: overrideValue; // set された値をそのまま返す
},
set: ({ set }, newValue) => {
overrideValue = newValue;
set(setCount, count => count + 1);
}
});
}
実装のポイント
だから newValue の型は T | DefaultValue になっている
今回はその値を、データ兼フラグとして活用させてもらう
overrideValue が DefaultValue ならリセット済み、そうでなければ任意の値で上書きされていることを表す
setCount は、set または reset が行われた時に、Selector の get ハンドラをコールさせるためのトリガー
だから値はなんでもよくて、分かりやすく回数にしているが、実は同じ値を永久にセットし続けても問題ない(はず)