jpskill.com
🛠️ 開発・MCP コミュニティ

react-zustand-patterns

Zustand state management patterns for React. Use when working with Zustand stores, debugging state timing issues, or implementing async actions. Works for both React web and React Native.

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o react-zustand-patterns.zip https://jpskill.com/download/17854.zip && unzip -o react-zustand-patterns.zip && rm react-zustand-patterns.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17854.zip -OutFile "$d\react-zustand-patterns.zip"; Expand-Archive "$d\react-zustand-patterns.zip" -DestinationPath $d -Force; ri "$d\react-zustand-patterns.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して react-zustand-patterns.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → react-zustand-patterns フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

ReactのためのZustandパターン

問題提起

Zustandのシンプルさの裏には、重要なタイミングに関する詳細が隠されています。set()は同期的ですが、Reactの再レンダリングはバッチ処理されます。getState()は古いクロージャを回避します。ストア内の非同期アクションは、注意深く処理する必要があります。これらの内部構造を理解することで、微妙なバグを防ぐことができます。


パターン: set()は同期的、レンダリングはバッチ処理

問題: set()の直後にReactのために状態が「準備完了」になっていると想定すること。

const useStore = create((set, get) => ({
  count: 0,

  increment: () => {
    set({ count: get().count + 1 });
    // 状態はここで更新されます (setは同期的)
    console.log(get().count); // ✅ 新しい値が表示されます

    // しかし、Reactはまだ再レンダリングされていません
    // コンポーネントは、次のレンダリングサイクルまで古い値を参照します
  },
}));

重要な洞察:

  • set()はストアを同期的に更新します
  • getState()は新しい値を即座に反映します
  • Reactコンポーネントは非同期的に(バッチ処理で)再レンダリングします

これが重要な場合:

  • 複数の状態更新を連鎖させる場合
  • 更新後に状態を検証する場合
  • コンポーネントの値が「古い」状態をデバッグする場合

パターン: getState()は古いクロージャを回避

問題: コールバック関数と非同期関数は、作成時に状態をキャプチャします。get()またはgetState()を使用すると、常に現在の状態を取得できます。

const useStore = create((set, get) => ({
  data: {},

  // 間違い - クロージャが古い状態をキャプチャします
  saveDataBad: (id: string, value: number) => {
    setTimeout(() => {
      // 誰かが`data`をパラメータとして渡した場合、それは古いものになります
    }, 1000);
  },

  // 正しい - 現在の状態には常にget()を使用します
  saveData: async (id: string, value: number) => {
    await someAsyncOperation();
    // awaitの後、現在の状態を確実にするためにget()を使用します
    const currentData = get().data;
    set({ data: { ...currentData, [id]: value } });
  },
}));

// コンポーネント内 - 同じ原則
function Component() {
  const data = useStore((s) => s.data);

  const handleSave = async () => {
    await delay(1000);
    // ここでのdataは古いです!レンダリング時にキャプチャされました

    // 現在の値にはgetState()を使用します
    const current = useStore.getState().data;
  };
}

ルール: awaitの後は、get()またはgetState()を使用し、クロージャでキャプチャされた値に決して依存しないでください。


パターン: ストア内の非同期アクション

問題: 非同期アクションには、明示的なasync/awaitと、await後の慎重な状態の読み取りが必要です。

const useStore = create((set, get) => ({
  loading: false,
  data: null,
  error: null,

  // 間違い - asyncキーワードがなく、競合状態が発生しやすい
  fetchDataBad: (id: string) => {
    set({ loading: true });
    api.fetch(id).then((data) => {
      set({ data, loading: false });
    });
    // すぐに返されるため、呼び出し元はawaitできません
  },

  // 正しい - 適切な非同期アクション
  fetchData: async (id: string) => {
    set({ loading: true, error: null });

    try {
      const data = await api.fetch(id);
      // 必要に応じて、await後に状態を再読み込みします
      if (get().loading) { // まだ読み込み状態にあるか確認します
        set({ data, loading: false });
      }
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
}));

// 呼び出し元は適切にawaitできます
await useStore.getState().fetchData('123');

パターン: セレクターの安定性

問題: 新しいオブジェクトを作成するセレクターは、不要な再レンダリングを引き起こします。

// 間違い - レンダリングごとに新しいオブジェクトを作成します
const data = useStore((state) => ({
  name: state.name,
  count: state.count,
}));

// 正しい - 複数のセレクターを使用します
const name = useStore((state) => state.name);
const count = useStore((state) => state.count);

// または - shallow比較を使用します (Zustand 4.x)
import { shallow } from 'zustand/shallow';

const { name, count } = useStore(
  (state) => ({ name: state.name, count: state.count }),
  shallow
);

// Zustand 5.x - useShallowフックを使用します
import { useShallow } from 'zustand/react/shallow';

const { name, count } = useStore(
  useShallow((state) => ({ name: state.name, count: state.count }))
);

パターン: 派生状態

問題: セレクターで派生値を計算するのと、それらを格納するのとではどちらが良いか。

const useStore = create((set, get) => ({
  items: [],

  // 間違い - 古くなる可能性のある派生状態を格納しています
  totalItems: 0,
  updateTotalItems: () => {
    set({ totalItems: get().items.length });
  },
}));

// 正しい - セレクターで計算します (常に最新)
const totalItems = useStore((state) => state.items.length);

// 計算コストが高い場合は、ストアの外でメモ化します
import { useMemo } from 'react';

function Component() {
  const items = useStore((state) => state.items);
  const expensiveResult = useMemo(() => {
    return computeExpensiveAnalysis(items);
  }, [items]);
}

パターン: サイドエフェクトのためのストアのサブスクリプション

問題: Reactコンポーネントの外部で状態の変化に反応する必要がある。

// 特定の状態の変化をサブスクライブします
const unsubscribe = useStore.subscribe(
  (state) => state.data,
  (data, prevData) => {
    console.log('Data changed:', { prev: prevData, current: data });
    // ストレージへの永続化、分析の送信など
  },
  { equalityFn: shallow }
);

// Zustand 4.xでは、subscribeWithSelectorミドルウェアを使用します
import { subscribeWithSelector } from 'zustand/middleware';

const useStore = create(
  subscribeWithSelector((set, get) => ({
    data: {},
    // ...
  }))
);

パターン: Zustandストアのテスト

問題: テストでは、ストアの状態をリセットし、非同期フローを検証する必要があります。


// リセット機能付きのストア
const initialState = {
  data: {},
  loading: false,
};

const useStore = create((set, get) => ({
  ...initialState,

  // アクション...

  // テストのためにリセット
  _reset: () => set(initialState),
}));

// テスト
describe('Data Store', () => {
  beforeEach(() => {
    useStore.getState()._reset();
  });

  it('データを正しくフェッチする', async () => {
    const store = useStore.getState();

    await store.fetchData('123');

    expect(useStore.getState().data).toBeDefined();
    expect(useStore.getState().loading).toBe(false);


(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Zustand Patterns for React

Problem Statement

Zustand's simplicity hides important timing details. set() is synchronous, but React re-renders are batched. getState() escapes stale closures. Async actions in stores need careful handling. Understanding these internals prevents subtle bugs.


Pattern: set() is Synchronous, Renders are Batched

Problem: Assuming state is "ready" for React immediately after set().

const useStore = create((set, get) => ({
  count: 0,

  increment: () => {
    set({ count: get().count + 1 });
    // State IS updated here (set is sync)
    console.log(get().count); // ✅ Shows new value

    // But React hasn't re-rendered yet
    // Component will see old value until next render cycle
  },
}));

Key insight:

  • set() updates the store synchronously
  • getState() immediately reflects the new value
  • React components re-render asynchronously (batched)

When this matters:

  • Chaining multiple state updates
  • Validating state after update
  • Debugging "stale" component values

Pattern: getState() Escapes Stale Closures

Problem: Callbacks and async functions capture state at creation time. Using get() or getState() always gets current state.

const useStore = create((set, get) => ({
  data: {},

  // WRONG - closure captures stale state
  saveDataBad: (id: string, value: number) => {
    setTimeout(() => {
      // If someone passed `data` as a parameter, it would be stale
    }, 1000);
  },

  // CORRECT - always use get() for current state
  saveData: async (id: string, value: number) => {
    await someAsyncOperation();
    // After await, use get() to ensure current state
    const currentData = get().data;
    set({ data: { ...currentData, [id]: value } });
  },
}));

// In components - same principle
function Component() {
  const data = useStore((s) => s.data);

  const handleSave = async () => {
    await delay(1000);
    // data here is stale! Captured at render time

    // Use getState() for current value
    const current = useStore.getState().data;
  };
}

Rule: After any await, use get() or getState() - never rely on closure-captured values.


Pattern: Async Actions in Stores

Problem: Async actions need explicit async/await and careful state reads after awaits.

const useStore = create((set, get) => ({
  loading: false,
  data: null,
  error: null,

  // WRONG - no async keyword, race condition prone
  fetchDataBad: (id: string) => {
    set({ loading: true });
    api.fetch(id).then((data) => {
      set({ data, loading: false });
    });
    // Returns immediately, caller can't await
  },

  // CORRECT - proper async action
  fetchData: async (id: string) => {
    set({ loading: true, error: null });

    try {
      const data = await api.fetch(id);
      // Re-read state after await if needed
      if (get().loading) { // Check we're still in loading state
        set({ data, loading: false });
      }
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
}));

// Caller can properly await
await useStore.getState().fetchData('123');

Pattern: Selector Stability

Problem: Selectors that create new objects cause unnecessary re-renders.

// WRONG - creates new object every render
const data = useStore((state) => ({
  name: state.name,
  count: state.count,
}));

// CORRECT - use multiple selectors
const name = useStore((state) => state.name);
const count = useStore((state) => state.count);

// OR - use shallow comparison (Zustand 4.x)
import { shallow } from 'zustand/shallow';

const { name, count } = useStore(
  (state) => ({ name: state.name, count: state.count }),
  shallow
);

// Zustand 5.x - use useShallow hook
import { useShallow } from 'zustand/react/shallow';

const { name, count } = useStore(
  useShallow((state) => ({ name: state.name, count: state.count }))
);

Pattern: Derived State

Problem: Computing derived values in selectors vs storing them.

const useStore = create((set, get) => ({
  items: [],

  // WRONG - storing derived state that can become stale
  totalItems: 0,
  updateTotalItems: () => {
    set({ totalItems: get().items.length });
  },
}));

// CORRECT - compute in selector (always fresh)
const totalItems = useStore((state) => state.items.length);

// For expensive computations, memoize outside the store
import { useMemo } from 'react';

function Component() {
  const items = useStore((state) => state.items);
  const expensiveResult = useMemo(() => {
    return computeExpensiveAnalysis(items);
  }, [items]);
}

Pattern: Store Subscriptions for Side Effects

Problem: Need to react to state changes outside React components.

// Subscribe to specific state changes
const unsubscribe = useStore.subscribe(
  (state) => state.data,
  (data, prevData) => {
    console.log('Data changed:', { prev: prevData, current: data });
    // Persist to storage, send analytics, etc.
  },
  { equalityFn: shallow }
);

// In Zustand 4.x with subscribeWithSelector middleware
import { subscribeWithSelector } from 'zustand/middleware';

const useStore = create(
  subscribeWithSelector((set, get) => ({
    data: {},
    // ...
  }))
);

Pattern: Testing Zustand Stores

Problem: Tests need to reset store state and verify async flows.

// Store with reset capability
const initialState = {
  data: {},
  loading: false,
};

const useStore = create((set, get) => ({
  ...initialState,

  // Actions...

  // Reset for testing
  _reset: () => set(initialState),
}));

// Test
describe('Data Store', () => {
  beforeEach(() => {
    useStore.getState()._reset();
  });

  it('fetches data correctly', async () => {
    const store = useStore.getState();

    await store.fetchData('123');

    expect(useStore.getState().data).toBeDefined();
    expect(useStore.getState().loading).toBe(false);
  });
});

Pattern: Debugging State Changes

Problem: Tracking down when/where state changed unexpectedly.

// Add logging middleware
import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools(
    (set, get) => ({
      // ... your store
    }),
    { name: 'MyStore' }
  )
);

// Manual logging for specific debugging
const useStore = create((set, get) => ({
  data: {},

  saveData: (id: string, value: number) => {
    console.log('[saveData] Before:', {
      id,
      value,
      currentData: get().data,
    });

    set((state) => ({
      data: { ...state.data, [id]: value },
    }));

    console.log('[saveData] After:', {
      data: get().data,
    });
  },
}));

Pattern: Persist Middleware

Problem: Persisting state across sessions.

import { persist } from 'zustand/middleware';

// Web - localStorage
const useStore = create(
  persist(
    (set, get) => ({
      preferences: {},
      setPreference: (key, value) =>
        set((state) => ({
          preferences: { ...state.preferences, [key]: value }
        })),
    }),
    {
      name: 'app-preferences',
      // Optional: choose what to persist
      partialize: (state) => ({ preferences: state.preferences }),
    }
  )
);

Common Pitfalls

Pitfall Solution
Stale closure after await Use get() after every await
Selector returns new object Use shallow or multiple selectors
Action not awaitable Add async keyword, return promise
State seems stale in component Component hasn't re-rendered yet - use getState() for immediate reads
Can't find when state changed Add devtools middleware or manual logging

Zustand 5.x Migration Notes

If upgrading from 4.x:

// 4.x - shallow from main package
import { shallow } from 'zustand/shallow';

// 5.x - useShallow hook for React
import { useShallow } from 'zustand/react/shallow';

// 4.x - type parameter often needed
const useStore = create<StoreType>()((set, get) => ({...}));

// 5.x - improved type inference
const useStore = create((set, get) => ({...}));