Claude CodeでReduxからZustandへ移す — ストア単位の段階移行
ReduxからZustandへの移行をClaude Codeに任せる手順です。先に画面単位のテストで挙動を固定し、ストアを1つずつ置き換え、テスト間のリセットとCLAUDE.mdの規約まで設定例で示します。
ReduxからZustandへの移行をClaude Codeに任せるときは、「全部を一度に書き換えて」と頼まず、ストア1つを単位にして、置き換える前に挙動を固定するテストを書かせます。変更が小さければ、テストが落ちたときに原因の範囲も小さく済みます。
この記事では、Reduxのストアが複数あるReactアプリを想定し、移行の順番、先に書かせるテストの種類、Zustand側で必要になるテスト間のリセット、CLAUDE.mdに置く規約を順に組みます。プロンプトは例示で、Claudeが実際に返す出力ではありません。
先に挙動を固定する理由と、そのテストの種類
Common workflowsのリファクタ手順は、変更後に「run tests for the refactored code」とテストを回す流れで、小さくテスト可能な単位で進めるよう勧めています。移行も同じで、置き換えた結果が正しいかどうかは、置き換える前から通っているテストでしか判定できません。
ベストプラクティスのページも、Claudeには完了を判定する手段を渡すよう説いています。バグ修正の例では、再現する失敗テストを書かせてから直させています。出力を根拠に確認する、という点も同じです。移行では「失敗テストを書く」の代わりに「いま通っているテストを増やす」ことから始めます。
固定するテストは、Reduxの内部ではなく画面の挙動に向けます。Reduxのテストガイドは、本物のストアを持つ <Provider> で部品を描画し、APIだけをモックして、UIが更新されることを確かめる統合テストを勧めています。セレクターやReact-Reduxのフックはモックしないよう、同じページに書かれています。
この方針は移行と相性が良い。画面に対するテストは、Reduxを使っているかどうかを知りません。ストアの実装をZustandに替えても、描画する側の Provider を差し替えるだけで、同じ検証をそのまま流用できます。逆にreducerを直接呼ぶ単体テストは、置き換えた時点で対象ごと無くなります。
移行の全体像
ストアごとに、次の流れを繰り返します。
ストア1つぶんの移行サイクル
- 1
現状を読ませる
plan modeで、そのストアを使う部品・セレクター・thunkを列挙させます。まだ編集はさせません。
- 2
挙動テストを足す
画面操作を起点にした統合テストを足し、Redux実装のまま通ることを確かめます。
- 3
Zustandへ置き換える
ストア本体と、それを読む部品だけを書き換えます。他のストアには触れません。
- 4
同じテストで検証する
追加したテストと既存テストの出力を読ませ、失敗を直してから次のストアへ進みます。
plan modeは、claude --permission-mode plan で起動するか、セッション中に Shift+Tab を押して切り替えます。Claudeはファイルを読んで計画を出し、承認するまで編集しません。移行の最初の一手として、ストアの依存関係の洗い出しを頼むには向いた機能です。
手順1: 移す順序を決める
最初に頼むのは移行ではなく棚卸しです。例えば次のように頼みます。
src/store 配下のReduxストアを一覧にして。ストアごとに
- 使っている部品の数
- 他のストアのstateをselectorで読んでいるか
- thunkやmiddlewareへの依存
を表にして。編集はしない。結果を見て、他のストアから参照されず、部品数が少なく、thunkの少ないものから順に並べます。依存の少ないストアほど、置き換えが他に波及しません。他のストアのstateを読んでいるストアは、読まれる側を先に移す必要があります。この順序は、返ってきた表を人が確認して決めます。Claudeの提案をそのまま採用せず、1つめのストアは「小さくて、画面から触れる」ものを選ぶと、手順の確認を兼ねられます。
手順2: 画面単位のテストを先に書かせる
次に、選んだストアに対する挙動テストを足します。Common workflowsの「Work with tests」の流れに沿って、未カバーの箇所の洗い出し、テストの雛形、エッジケース、実行の順で頼めます。
cartストアを使っている画面について、まだテストがない操作を挙げて。
その後、実際のReduxストアをProviderで包んで描画し、
ボタン操作から表示の変化までを確かめるテストを足して。
ストアの内部やselectorはモックしないこと。APIだけをモックする。テストを足したら、Redux実装のまま全件通ることを必ず確かめます。ここで落ちるなら、テストの書き方に問題があり、移行はまだ始められません。
Claudeは既存のテストファイルを読んで、使われているフレームワークや書き方に合わせたテストを書きます。Vitestで回しているなら、watchモードで止まらない書き方をCLAUDE.mdに置くとよく、手順はClaude CodeでVitestを回す記事にまとめています。Jestの場合はJest 30へのアップグレード手順が近い話題です。
手順3: ストアを置き換える
挙動が固定できたら、置き換えです。Zustandの基本形は、create に set を受け取る関数を渡し、stateと更新関数を同じオブジェクトに置く書き方です。ストアそのものがフックなので、Provider で囲む必要がありません。
import { create } from 'zustand'
type CartState = {
items: string[]
add: (id: string) => void
clear: () => void
}
export const useCartStore = create<CartState>((set) => ({
items: [],
add: (id) => set((state) => ({ items: [...state.items, id] })),
clear: () => set({ items: [] }),
}))部品側は、useSelector の代わりにストアのフックへselectorを渡します。
const items = useCartStore((state) => state.items)
const add = useCartStore((state) => state.add)mapStateToProps のように複数の値をまとめて取りたいときは、READMEが useShallow を紹介しています。オブジェクトを返すselectorで毎回再描画させない目的です。
Reduxのreducerとaction typeをそのまま残したいときは、中間段階が作れます。ZustandのREADMEは、reducerを set から呼ぶ書き方と、zustand/middleware の redux ミドルウェアを紹介しています。このミドルウェアは、reducerと初期stateを渡すと、dispatch 関数をstate自身に載せます。
import { create } from 'zustand'
import { redux } from 'zustand/middleware'
export const useCartStore = create(redux(cartReducer, initialState))こうすると、部品側のdispatchとaction typeを残したままストアの実体だけを先に替えられます。action typeを関数に開く作業は、次の別コミットに分けます。移行が2段になる代わりに、各段の差分が小さくなります。これは進め方の一例で、Zustandのドキュメントに段階移行の手順として載っているものではありません。
置き換えを頼むプロンプトでは、範囲を明示します。
cartストアだけをZustandに置き換えて。
- Reduxのcartスライスと、それを読む部品だけを書き換える
- 他のストアと、無関係な部品には触れない
- 終わったら全テストを実行して、出力を貼って手順4: テスト間のリセットを入れる
ここが、Redux時代のテストにはなかった落とし穴です。Reduxのテストは、テストごとに新しいストアを作るのが普通です。Zustandはストアがモジュールの値として作られ、Provider もありません。何もしなければ、あるテストで増えた商品が次のテストに残ります。
Zustandのテストガイドは、各テストの後に全ストアを初期状態へ戻す __mocks__/zustand.ts を載せています。create を包み、ストア作成時に getInitialState() を控えておき、afterEach で setState(initialState, true) を呼ぶ作りです。JestとVitestで少し書き方が違い、Vitestでは vi.importActual を使います。Vitestはプロジェクトの root を変えている場合に、__mocks__ の置き場所もずらす必要があると注意されています。
このファイルはClaudeに一から考えさせず、testing.mdの内容を渡して設置させるほうが確実です。
Zustandの公式テストガイド(testing.md)にある __mocks__/zustand.ts を、
うちのテストランナー(Vitest)向けに設置して。
設置後、cartのテストを2つ連続で走らせて、状態が持ち越されないことを確かめて。テスト同士で順序依存が出ていないかは、ファイル単体と全件の両方で回して確かめます。
手順5: CLAUDE.mdに移行の規約を置く
ストアを複数回に分けて移すと、セッションをまたいで方針がぶれがちです。毎回の指示に書く内容は、CLAUDE.mdに置きます。ベストプラクティスのページは、CLAUDE.mdに書くのは「Claudeがコードを読んでも分からないこと」に絞るよう勧めており、検証のコマンドもその1つです。
## 状態管理の移行(Redux → Zustand)
- 1回の変更で移すストアは1つだけ
- 移行前に、画面単位の統合テストを足し、Redux実装のまま通ることを確認する
- 新しいコードは `src/stores/` のZustandストアを使い、Reduxを追加しない
- 検証: `npx vitest run`(全件)、`npx tsc --noEmit`
- 完了報告では、実行したコマンドとその出力を貼る「出力を貼る」の一行は、完了の根拠を人が読むためです。同じページが、「テストが通った」という申告だけでは情報が少なく、出力を読むほうが再実行より速いと説明しています。
移行後にスライスをまとめるか
ストアが増えてきたら、Zustandの「スライス」パターンで1つのストアにまとめる選択肢があります。スライスのガイドは、小さなストアを create の中で束ねて1つの境界づけられたストアにする方法を示しています。注意点として、ミドルウェアは束ねたストアにだけ適用し、個々のスライスの内側には適用しないよう書かれています。
まとめるかどうかは、移行とは別の判断です。段階移行の途中で統合まで進めると、テストが落ちたときに原因が分かれます。ストアを移し終え、全件が通ってから、別の変更として検討するのが安全です。
つまずきやすい点
- 移行の前にテストを足していない: 置き換え後に落ちたテストが、既存のテストなのか新しい挙動の誤りなのか判別できません。
- selectorやフックをモックしたテストを残す: Reduxのテストガイドが避けるよう書いている形で、Zustandに替えた時点で壊れます。画面単位のテストに書き直します。
- テスト間のリセット忘れ: 単体では通り、全件で落ちる形で現れます。
- 1回の指示で複数のストアを頼む: 差分が大きくなり、レビューで見落としが出ます。指示にストア名を1つだけ書きます。
- Claudeの「通りました」だけを受け取る: 実行したコマンドと出力を貼らせます。
まとめ
Reduxの内部に結びついたテストは移行で消えますが、画面の挙動に向けたテストは残ります。先にそちらを厚くしておけば、ストアの置き換えは1つずつ、同じ検証で確かめながら進められます。Claude Codeには範囲を1ストアに絞って渡し、plan modeで洗い出し、CLAUDE.mdに規約を置いておくと、セッションが変わっても同じ手順で続けられます。
React Routerのように型検査を完了条件に使う構成は、React Router v7のtypegen記事に近い進め方です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。