Reactで軽量な状態管理ライブラリを調べるとRecoilが有力そうなので試してみた。
その際に、LocalStorageや非同期処理でDBに保存する処理などを Atom Effects を利用してView側のロジックから引き剥がせたので、そのコードを簡略化したものを記載しておく。
ほとんど公式と同じです。
前提条件
- React: 18.2.0
- Recoil: 0.7.7
ローカルストレージに永続化させるサンプル実装
- ユーザーは画面で2つの状態のどちらかを選択できる
- 次回以降も反映させるためローカルストレージを用いて状態を永続化する(初回読み込み時に反映)
このような仕様を満たすために、Recoilを用いると次のような実装ができた。
- state.ts // 状態管理用
import { atom } from "recoil";
const localStorageKey = "SAMPLE_KEY"
type SampleType = "A" | "B";
type SampleStateProps = {
hoge: SampleType;
};
export const SampleState = atom<SampleStateProps>({
key: "sample",
default: {
hoge: "A",
},
effects: [
({ onSet, setSelf, trigger }) => {
if (trigger === "get") {
// 初回読み込み時に実行される
// ローカルストレージから取得し、値が存在すれば state に反映
const savedValue = localStorage.getItem(localStorageKey);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
}
// state が更新されたら実行される
onSet((newValue) => {
// ローカルストレージに保存
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
});
},
],
});
effects は配列を受け取るようになっており、用途に応じて複数渡すことで、状態が更新された際の副作用の処理などを複数登録しておける。
今回は初回読み込み時の処理と、状態が更新されたらローカルストレージにも反映させるようにした。
- app.tsx // 表示用
import { useRecoilState } from "recoil";
import { SampleState } from "./state";
const App = () => {
const [state, setState] = useRecoilState(SampleState);
const turnToA = () => setState({ hoge: "A" });
const turnToB = () => setState({ hoge: "B" });
return (
<div>
<p>{state.hoge}</p>
<div>
<button onClick={turnToA}>turn to A</button>
</div>
<div>
<button onClick={turnToB}>turn to B</button>
</div>
</div>
);
};
export default App;
動かしてみると確かにローカルストレージに永続化し、次回読み込み時も反映された。
View側からは単に状態更新を行っているだけに見えるので、余計なロジックを気にしなくても良くなった。
Tips
Effects に配置する処理は AtomEffect を用いて別途関数として定義しておけるので、複数登録する場合は役割に応じて分割しておくと保守しやすいと感じた。
import { AtomEffect, atom } from "recoil";
...省略
const localStorageEffect: (key: string) => AtomEffect<SampleStateProps> = (key: string) => ({setSelf, onSet, trigger}) => {
const getByLocalStorage = async () => {
// ローカルストレージから取得し、値が存在すれば state に反映
const savedValue = await localStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
};
if (trigger === "get") {
// 初回読み込み時に実行される
getByLocalStorage();
}
// state が更新されたら実行される
onSet((newValue) => {
// ローカルストレージに保存
localStorage.setItem(key, JSON.stringify(newValue));
});
};
export const SampleState = atom<SampleStateProps>({
key: "sample",
default: {
hoge: "A",
},
effects: [
localStorageEffect(localStorageKey),
// ロギングなどを別途追加する...
],
});
DBに保存する場合の処理は割愛させていただくが、基本的には onSet の状態更新のイベントを受けて副作用的に更新する。
ただ今回のローカルストレージとは異なり、保存に失敗するケースが発生するので注意が必要になる。
場合にもよるが、DB保存専用の状態を設けて "保存中" | "成功" | "失敗" のような状態で管理しつつ、ユーザーフレンドリーな表示をしつつ、リカバリー可能な処理を設けるのが良いだろう。
見てくださりありがとうございました。