kt-tech.blog

画像

【実装】React Context → zustand移行ガイド(persist + レガシー値マイグレーション)

Share
💡
React ContextをzustandのpersistミドルウェアでlocalStorageに永続化する形に移行した際の手順と、レガシー値のマイグレーションパターンを紹介します。

はじめに

React Contextで管理していたキャラクター選択と言語設定をzustandに移行しました。Contextの問題点(Provider地獄、不要な再レンダリング)を解消しつつ、localStorageへの永続化も追加しています。

この記事でわかること

  • React Context → zustandの具体的な移行手順

  • persist ミドルウェアでlocalStorage永続化する方法

  • レガシー値のマイグレーション(merge関数)パターン

  • Provider削除によるlayout.tsxの簡素化

Before: React Context

// contexts/CharacterContext.tsx
const CharacterContext = createContext<CharacterContextType>(...);

export function CharacterProvider({ children }) {
  const [selected, setSelected] = useState("akari");
  // localStorage読み込み、レガシー値変換...
  return (
    <CharacterContext.Provider value={{...}}>
      {children}
    </CharacterContext.Provider>
  );
}

// layout.tsx — Provider地獄
<AuthSessionProvider>
  <LanguageProvider>
    <CharacterProvider>
      {children}
    </CharacterProvider>
  </LanguageProvider>
</AuthSessionProvider>

After: zustand + persist

// stores/useCharacterStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface CharacterStore {
  selectedSlug: string;
  setSelectedSlug: (slug: string) => void;
}

const LEGACY_SLUG_MAP: Record<string, string> = {
  blue: "haruto",
  red: "akari",
};

export const useCharacterStore = create<CharacterStore>()(
  persist(
    (set) => ({
      selectedSlug: "akari",
      setSelectedSlug: (slug) => set({ selectedSlug: slug }),
    }),
    {
      name: "selectedCharacter", // localStorageキー
      merge: (persisted, current) => {
        const p = persisted as Partial<CharacterStore> | undefined;
        const raw = p?.selectedSlug ?? current.selectedSlug;
        return { ...current, selectedSlug: LEGACY_SLUG_MAP[raw] ?? raw };
      },
    },
  ),
);

レガシー値マイグレーションのポイント

⚠️
既存ユーザーのlocalStorageに古い形式の値(“blue”, “red”)が残っている場合、merge関数でマイグレーションします。Mapで変換すればletも不要で簡潔に書けます。

persist の merge オプションは、localStorageから読み込んだ値とデフォルト値をマージする関数です。ここでレガシー値の変換を行うことで、既存ユーザーのデータを壊さずに移行できます。

layout.tsxの変更

// Before: Provider地獄
<AuthSessionProvider>
  <LanguageProvider>
    <CharacterProvider>{children}</CharacterProvider>
  </LanguageProvider>
</AuthSessionProvider>

// After: Providerが不要に
<AuthSessionProvider>
  {children}
</AuthSessionProvider>

移行チェックリスト

  1. zustand storeファイルを作成(persist設定、レガシーマイグレーション含む)

  2. 各コンポーネントのimportをuseContext → useXxxStore に置換

  3. layout.tsxからProvider wrapperを削除

  4. 旧Contextファイルを削除

  5. ビルド・動作確認(localStorage既存値の互換性確認含む)

まとめ

  • zustandはProvider不要で、import して使うだけ → Provider地獄解消

  • persist ミドルウェアで localStorage永続化がワンライナー

  • merge関数でレガシー値のマイグレーションを宣言的に記述できる

  • storeにはUIに必要な最小限のstate(slug等)だけ持ち、派生データはhookで計算する