これは ZOZO Advent Calendar 2022 カレンダー Vol.4 の 17 日目の記事です。
React でグローバルな状態管理するとき、皆さんは何を使うでしょうか。
状態管理ライブラリには Redux, Recoil, jotai, Zustand など様々ありますが、わざわざ入れるまでもないかと Context を使っている人も多いと思います。
Context を用いた自作の状態管理は、スコープを作れたり、文献が多いなどのメリットもありますが React への理解が浅いと余計なレンダリングを招くなどの罠もあります。(今回は触れません。React Context アンチパターン などで検索するとでてきます)
本記事は Context を用いた状態管理を否定するものではありません。
本記事では、 useSyncExternalStore
を用いた状態管理を作ることで、 useSyncExternalStore
とそれを使った状態管理ライブラリへの理解を深めることがゴールです。
useSyncExternalStore
useSyncExternalStore は React18 から導入されたフックで、下位互換のある use-sync-external-store
パッケージとしても提供されています。公式のドキュメントでは ライブラリの製作者向け という記述がありますが、覚えておいて損はないでしょう。
外部データソースから読み出しやデータの購読を行うために推奨されるフックです。
このフックを用いることで、簡単に状態管理が行えるようになりました。
Zustand や nanostores などがフックのみで(ツリーの上層でコンポーネントを配置せずに、かつ一意の文字列の Key などを設定せずに)利用できるのはこのフックによる影響が大きいです。
jotaiは実は使っていませんが...
API
useSyncExternalStore は 3 つの引数を取ります。
-
subscribe
: 状態の変化を通知するためのコールバックを登録する関数 -
getSnapshot
: 現在の状態の値を返す関数 -
getServerSnapshot
: サーバーサイドレンダリング時の値を返す関数(任意)
subscribe
は、返り値として unsubscribe な通知を中止する関数を返す必要があります。
次は、具体的なコードとしてオレオレ状態管理を作ってみます。
オレオレ状態管理してみる
jotai ライクな API で必要最低限の処理を記述するとこうなりました。
export const atom = (initialState) => {
let state = initialState;
let callbacks = new Set();
return {
get: () => state,
set: (value) => {
state = typeof value === "function" ? value(state) : value;
callbacks.forEach((cb) => cb());
},
subscribe: (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
},
};
};
export const useAtomValue = (atom) => {
return useSyncExternalStore(atom.subscribe, atom.get);
};
export const useSetAtom = (atom) => {
return atom.set;
};
export const useAtom = (atom) => {
return [useAtomValue(atom), useSetAtom(atom)];
};
1状態1 atom な状態管理がトレンドっぽい(要出典)ので同様に atom
で状態定義、 useAtom
などでアクセスという形です。
subscribe
で登録されたコールバック関数を Set で管理し、 set
で状態が更新された際に登録されたコールバック関数を呼ぶことで、状態の変化が通知されます。
このままでは SSR などが考慮されないので実用性には欠けますが、 Playground で React を触る時には使えるかもしれません。
TypeScript を用いたデモ
この形式を応用することで、ResizeObserver や IntersectionObserver などの DOM イベント、非同期な操作を含む Reducer などのカスタムフック化もロジックの分離が容易になります。
おわりに
この記事では Context を使わない自作状態管理について考えました。
useSyncExternalStore
を用いることで煩雑な Context 管理から解放されましょう。
また、多くの本番環境では(バンドルサイズの極限のチューニングが必要でない限り1)Context による状態管理や今回の手法を用いずに Zustand, jotai, nanostores など著名なライブラリを選定したほうが結果的に安く済むと思います。
-
この場合そもそも React を使わない選択肢を考えるべきです ↩