zustand-state-management について
ReactとTypeScriptアプリでのZustandによる状態管理を扱うスキルです。共有クライアント状態、型付きストア、セレクター、ミドルウェア、SSR、テストに焦点を当てます。
主な機能
- 01状態の所有先に関する指針
- 02小規模ストアと型付きスライス
- 03セレクターの性能改善
- 04永続化とDevToolsミドルウェア
- 05SSR対応ストアとテスト
活用シーン
- 01Reactコンポーネント間の共有状態設計
- 02選択範囲を絞ったセレクターによる再レンダー削減
- 03ストアアクションとミドルウェアのテスト
- 04SSRアプリでの永続化状態の処理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill zustand-state-managementインストール後、Claude Code で/zustand-state-managementを入力して読み込まれたか確認します。
詳細
Zustand 状態管理
このスキルでは、React および TypeScript アプリケーションで Zustand ストアを設計、使用する方法を扱います。状態の所有権の判断、ストアのスライス分割、セレクターのパフォーマンス、ミドルウェア、SSR、テストなどが含まれます。
状態の所有権
- 一時的な UI 状態は、最も近いコンポーネントに
useStateまたはuseReducerで保持します。デフォルトで Zustand に頼らないでください。 - 共有可能なフィルター、ページネーション、タブ、検索パラメーターには URL の状態を使用します。
- Zustand は、関連のないコンポーネント間で本当に共有する必要があるクライアント状態(認証セッションの表示状態、コマンドパレット、カートの下書き、エディターの状態など)にのみ使用します。
- サーバー状態には TanStack Query、SWR、RTK Query、またはプロジェクトに既存のデータレイヤーを使用します。オフライン対応や下書き編集の要件が文書化されている場合を除き、取得したサーバーデータを Zustand ストアに重複して保存しないでください。
- 関数型で宣言的なパターンを実装し、クラスは避けます。
isLoadingやhasErrorなど、補助動詞を使ったわかりやすい変数名を使用します。
ストアの設計
- 各ストアは状態と名前付きアクションとしてモデル化し、コンポーネントのコードから誤用される可能性のある、名前のないセッターを公開しないでください。
- アプリケーション全体を対象とする単一のグローバルストアではなく、小規模でドメインに特化したストアを保ちます。
- 大規模なストアは型付きスライスに分割し、ミドルウェアは合成したストアの境界にのみ適用します。
- 派生値は、状態にキャッシュする必要がない限り、セレクターまたは小規模な純粋ヘルパーとして保持します。
- デフォルトではシリアライズ可能なデータを保存し、DOM ノード、プロミス、ソケット、タイマーはストアの状態の外に置きます。
import { create } from 'zustand'
interface SidebarState {
isOpen: boolean
activePanelId: string | null
}
interface SidebarActions {
openPanel: (panelId: string) => void
close: () => void
toggle: () => void
}
type SidebarStore = SidebarState & SidebarActions
export const useSidebarStore = create<SidebarStore>()((set) => ({
isOpen: false,
activePanelId: null,
openPanel: (panelId) => set({ isOpen: true, activePanelId: panelId }),
close: () => set({ isOpen: false, activePanelId: null }),
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}))
コンポーネントでの使用とパフォーマンス
- 可能な限り小さなスライスを購読します。
useStore((state) => state.value)のように指定します。ストア全体の選択は避けてください。 - コンポーネントがすべてのフィールドを本当に必要とする場合を除き、セレクターなしでストアフックを呼び出さないでください。
- セレクターが複数の値を含むオブジェクトまたはタプルを返す場合は、
zustand/react/shallowのuseShallowを使用して、呼び出しごとに新しいオブジェクト参照が生成されることによる再レンダリングを防ぎます。 - セレクターは純粋で軽量に保ち、コストの高い導出処理はメモ化されたヘルパーに移します。
import { useShallow } from 'zustand/react/shallow'
import { useSidebarStore } from '@/stores/sidebar-store'
export function SidebarToggle() {
const { isOpen, toggle } = useSidebarStore(
useShallow((state) => ({
isOpen: state.isOpen,
toggle: state.toggle,
})),
)
return (
<button type="button" aria-expanded={isOpen} onClick={toggle}>
Toggle sidebar
</button>
)
}
TypeScript
- 共有ストアには状態とアクションの明示的なインターフェースを定義します。複雑なストアでは、単一のフラットなインターフェースではなく、それらを結合します(
type Store = State & Actions)。 anyは避け、外部データにはunknownと絞り込みを使用します。- 非同期アクションを含め、アクションのペイロードと戻り値に型を付けます。
- 関連性の低いブール値をいくつも使う代わりに、複雑なローカル状態には判別共用体を優先します。
- テスト、ユーティリティ、またはバニラストアファクトリーで必要になる場合は、ストアの状態型をエクスポートします。
ミドルウェア
更新
- 次の値が現在の状態に依存する場合は、
set((state) => nextState)のような関数形式を使用します。 - ネストされた状態は不変として扱います。ネストされた更新が大幅に簡潔になる場合にのみ
immerミドルウェアを導入して使用します。これを使わずにネストされたオブジェクトを直接変更しないでください。
永続化
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
// state and actions
}),
{ name: 'store-key' }
)
)
- 再読み込み後も保持する必要がある状態にのみ
persistを使用します。 - 些細な設定以外を永続化する場合は、
partialize、version、migrateを使用します。 - シークレット、アクセストークン、リフレッシュトークン、生の個人識別情報(PII)、その他の長期間有効な認証状態をブラウザーのストレージに永続化しないでください。
DevTools
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools((set) => ({
// state and actions
}))
)
- 複雑なフローには開発時に
devtoolsを使用し、重要なアクションにはトレースで識別できるわかりやすい名前を付けます。
React 外での購読
- コンポーネントのレンダリング外で詳細な更新が必要な React 外の購読には、
subscribeWithSelectorを使用します。
非同期アクションとエラー処理
- 非同期のストアアクションでは、クライアント専用のワークフロー、楽観的な下書き、またはローカルデバイス API を調整します。状態が明示的にクライアント所有でない限り、HTTP フェッチはプロジェクトのサーバー状態レイヤーに任せます。
- 非同期のクライアントワークフローは、場当たり的なブール値ではなく、
idle、pending、success、errorなどの明示的なステータスで表現します。 - 関数の冒頭で早期リターンやガード節を使ってエラーに対処します。非同期アクションでは try-catch を使い、意味のあるエラーメッセージを提示します。
- 再試行時やワークフローの終了時には、エラー状態を意図的にリセットします。
SSR と React Server Components
- React Server Components から、ブラウザー専用のストアを読み取ったり変更したりしないでください。
- SSR フレームワーク(Next.js App Router など)でサーバー上の状態初期化が必要な場合は、共有のモジュールレベルストアではなく、リクエストごとに vanilla ストアを作成します。
- 永続化されたストアでは、ストレージに保存された値をレンダリングする前に、ハイドレーションの不一致を防ぎます。
- ミドルウェアの設定外では、ストアモジュールから
window、document、ストレージに直接アクセスしないでください。
テスト
- コンポーネントとは独立してストアをテストします。可能であれば、React をレンダリングせずにストアアクションを直接テストします。
- テスト間では、初期状態を使ってストアをリセットします。
- セレクターとアクションは、コンポーネントの動作とは分けて検証します。UI ロジックを分離するコンポーネントテストでは、Zustand ストアをモックします。
- テストでは、取得したデータを Zustand 経由で流すのではなく、サーバー状態ライブラリをモックします。
- ミドルウェアの動作(永続化、devtools)は、コアのストアロジックとは分けてテストします。
アンチパターン
- アプリケーション全体で 1 つのグローバルストアを作成しないでください。
- 複数の離れたコンポーネントで同じ下書きを編集する場合を除き、Zustand にフォーム入力の状態を入れないでください。
- Immer ミドルウェアを使わずに、ネストされたオブジェクトを直接変更しないでください。
- Zustand をイベントバスとして使わないでください。明示的なコールバック、サービス、またはスコープ付きストアを優先します。
- プロジェクトですでにそのパターンを使っている場合を除き、Redux 形式のリデューサー、アクション定数、ディスパッチラッパーを導入しないでください。
コメント
Markdown 対応