React・ページを増やして、共有する

状態管理ライブラリを入れる(Zustand)

Contextで足りなくなるのはどこからか、を先に見てからZustandを入れます。セレクタで必要な部品だけ描き直す書き方、persistでlocalStorageに残す書き方、そしてReduxやJotaiとの選び分けまで扱います。

前回、ことりずかんのデータをContextに移しました。Contextは標準の機能なのでまずこれで足ります

ただ、実務のプロジェクトでは状態管理ライブラリが入っていることがよくあります。この回は「なぜ入るのか」を先に見てから、いちばん書きやすいZustandを入れます。

Contextで足りなくなるところ

Contextの弱点は1つだけです。値が1つでも変わると、そのContextを使っている部品が全部描き直されることです。

// この中に「鳥の一覧」「お気に入り」「検索語」を全部入れると……
<BirdsContext value={{ birds, favoriteIds, keyword }}>

検索欄に1文字打つ→keywordが変わる→鳥の一覧しか使っていないカードまで描き直される。この仕組みは、次の章のレンダリングの回で詳しく見ます。

回避する手はあります。Contextを役割ごとに分ける、useMemoで値を固定する。ただ手当てが増えてくると、それはもうライブラリがやっていることの自前実装です。

Contextのつらさ状態管理ライブラリの答え
値が変わると使っている部品が全部描き直される必要な値だけ購読する(セレクタ)
Providerで包む階層が深くなるProviderが要らない
保存(localStorage)を自分で書く設定1つ(persist)
Reactの外から値を読めないストアはReactの外にあるのでどこからでも読める

Zustandを入れる

npm install --save-exact zustand

お気に入りをZustandのストアに移してみます。

import { create } from 'zustand'

export const useFavoriteStore = create((set) => ({
  favoriteIds: [],
  toggleFavorite: (id) =>
    set((state) => ({
      favoriteIds: state.favoriteIds.includes(id)
        ? state.favoriteIds.filter((favoriteId) => favoriteId !== id)
        : [...state.favoriteIds, id],
    })),
}))

stateと、それを変える関数を同じ場所に書く——これがストアです。ContextのときのようにuseStateとProviderに分かれません。

使う側はこうです。

const favoriteIds = useFavoriteStore((state) => state.favoriteIds)
const toggleFavorite = useFavoriteStore((state) => state.toggleFavorite)

Providerがありません。main.jsxを触る必要もありません。ストアはReactの外にあり、コンポーネントは「この値を見せて」と申し込むだけです。

保存を1行で足す

localStorageへの保存も、persistで包むだけです。custom-hookの回で自分で書いた読み書きが、まるごと消えます。

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useFavoriteStore = create(
  persist(
    (set) => ({
      favoriteIds: [],
      toggleFavorite: (id) => set(/* 省略 */),
    }),
    { name: 'zukan-favorites-store' },   // localStorageのキー名
  ),
)

星を1つ押してから開発者ツールのApplication → Local Storageを見ると、こう入っています。

{"state":{"favoriteIds":[4]},"version":0}

versionが付いているのは、あとで保存の形を変えたときに古いデータを変換できるようにするためです(migrateという設定があります)。自前のlocalStorage処理でいちばん忘れられるのがこれです。

どれを入れるか

名前をよく見るものだけ並べます。

ライブラリ特徴出会う場面
Zustandストア1つ・Provider不要・学習量が少ない新規で入れるなら第一候補
Jotai小さな状態(アトム)を組み合わせる状態が細かく分かれる画面
Redux Toolkit決まりごとが多く、大人数でも崩れにくい既存の案件で現役。読めると強い
Context(標準)追加ゼロ。テーマ・ログイン中のユーザーなどめったに変わらない値まずここから

Reactの外から読める、という利点

ストアがReactの外にあるので、コンポーネントでない場所からも読み書きできます

// 通信処理の中など、フックが使えない場所から
const token = useAuthStore.getState().token
useAuthStore.getState().logout()

getState()購読しません(描き直しのきっかけになりません)。fetchの共通処理からログアウトさせたいときなどに効きます。あとのログインの回で実際に使います。

ReactContextで配らずに共有するまずContextで作った回。ライブラリを入れる前に、標準の機能でどこまで行けるかを見ています

公式ドキュメントも見てみる

zustand.docs.pmnd.rs外部サイトIntroduction - ZustandZustand公式の導入。createの形と、セレクタで購読するという考え方が最初に出てきます(英語)

このレッスンのまとめ

  1. Contextの弱点は値が1つ変わると使っている部品が全部描き直されること
  2. 状態管理ライブラリの答えはセレクタで必要な値だけ購読すること
  3. ZustandはProvider不要。ストアはReactの外にあり、コンポーネントは購読するだけ
  4. useFavoriteStore((state) => state.favoriteIds)カッコの中がセレクタ。全部取ると意味がなくなる
  5. persistでlocalStorage保存が1行。保存する範囲はpartializeで絞る
  6. サーバーのデータは入れない。それはTanStack Queryの担当
  7. 新規ならZustand/Jotai、既存案件ではRedux Toolkitが現役

次は、仕事で使うための章です。まずは「Reactはいつ描き直すのか」から。

やってみよう:お気に入りをZustandに移す

Contextで持っていたお気に入りを、ストアに移します。

  1. npm install --save-exact zustand
  2. src/stores/favoriteStore.jsを作り、favoriteIdstoggleFavoriteを持たせる
  3. BirdListPageBirdDetailPageで、Contextの代わりにuseFavoriteStoreから取る
  4. お気に入りのContext(Provider・useFavorites)を削除する
  5. persistで包み、リロードしても★が残ることを確認する
export const useFavoriteStore = create(
  persist(
    (set) => ({
      favoriteIds: [],
      toggleFavorite: (id) =>
        set((state) => ({
          favoriteIds: state.favoriteIds.includes(id)
            ? state.favoriteIds.filter((favoriteId) => favoriteId !== id)
            : [...state.favoriteIds, id],
        })),
    }),
    { name: 'zukan-favorites-store' },
  ),
)

5番のあと、開発者ツールのApplication → Local Storagezukan-favorites-storeが増えているのを見てください。中身が{"state":{"favoriteIds":[4]},"version":0}のようになっていれば成功です。

よくある質問

Contextがあるのに状態管理ライブラリが要るのですか?
小さいうちは要りません。Contextは値が1つ変わるとその値を使う部品がまとめて描き直されるので、頻繁に変わる状態を入れると重くなります。ライブラリは「必要な部分だけ描き直す」仕組みを持っているので、そこが問題になってから入れれば十分です。
ZustandとReduxはどちらを覚えればいいですか?
新しく始めるならZustandやJotaiが軽くて書きやすいです。ただし既存の案件ではRedux Toolkitが動いていることが多いので、読めるようにはしておくと役に立ちます。
サーバーから取ったデータもZustandに入れますか?
入れません。サーバーのデータはTanStack Queryのようなデータ取得ライブラリの担当で、Zustandはログイン状態やモーダルの開閉のような画面側の状態を持ちます。この線引きが今の実務での定番です。
Zustandのストアはコンポーネントの外に置いていいのですか?
はい。ストアはReactの外に置かれ、コンポーネントは購読するだけです。だからPropsのバケツリレーもProviderも要りません。