7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2022

Day 17

まだContextでグローバルステート管理してるの? useSyncExternalStoreを使う

Last updated at Posted at 2022-12-16

これは 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 つの引数を取ります。

  1. subscribe: 状態の変化を通知するためのコールバックを登録する関数
  2. getSnapshot: 現在の状態の値を返す関数
  3. 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 など著名なライブラリを選定したほうが結果的に安く済むと思います。

  1. この場合そもそも React を使わない選択肢を考えるべきです

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?