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

rn-zustand-patterns

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

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

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

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

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

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して rn-zustand-patterns.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → rn-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 Native向け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) => ({
  answers: {},

  // 間違い - 関数作成時に状態がキャプチャされる
  saveAnswerBad: (questionId: string, value: number) => {
    setTimeout(() => {
      const answers = get().answers; // ❌ これは問題ありません
      // しかし、誰かが`answers`をパラメータとして渡した場合...
    }, 1000);
  },

  // 正しい - 現在の状態には常にget()を使用する
  saveAnswer: async (questionId: string, value: number) => {
    await someAsyncOperation();
    // awaitの後、現在の状態を保証するためにget()を使用する
    const currentAnswers = get().answers;
    set({ answers: { ...currentAnswers, [questionId]: value } });
  },
}));

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

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

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

ルール: 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 comparisonを使用する (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) => ({
  answers: {},

  // 間違い - 古くなる可能性のある派生状態を保存する
  totalAnswers: 0,
  updateTotalAnswers: () => {
    set({ totalAnswers: Object.keys(get().answers).length });
  },

  // 正しい - セレクターで計算する (常に最新)
  // answers: {},  // ソースデータのみを保存する
}));

// セレクターは派生値を計算する
const totalAnswers = useStore((state) => Object.keys(state.answers).length);

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

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

パターン: 副作用のためのストアサブスクリプション

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

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

// Zustand 4.xでsubscribeWithSelectorミドルウェアを使用する場合
import { subscribeWithSelector } from 'zustand/middleware';

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

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

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


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

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

  // アクション...

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

// テスト
describe('As

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

Zustand Patterns for React Native

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) => ({
  answers: {},

  // WRONG - state captured at function creation
  saveAnswerBad: (questionId: string, value: number) => {
    setTimeout(() => {
      const answers = get().answers; // ❌ This is fine
      // But if someone passed `answers` as a parameter...
    }, 1000);
  },

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

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

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

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

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) => ({
  answers: {},

  // WRONG - storing derived state that can become stale
  totalAnswers: 0,
  updateTotalAnswers: () => {
    set({ totalAnswers: Object.keys(get().answers).length });
  },

  // CORRECT - compute in selector (always fresh)
  // answers: {},  // Just store the source data
}));

// Selector computes derived value
const totalAnswers = useStore((state) => Object.keys(state.answers).length);

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

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

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.answers,
  (answers, prevAnswers) => {
    console.log('Answers changed:', { prev: prevAnswers, current: answers });
    // 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) => ({
    answers: {},
    // ...
  }))
);

Pattern: Testing Zustand Stores

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

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

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

  // Actions...

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

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

  it('saves answers during retake flow', async () => {
    const store = useStore.getState();

    // Full async flow
    await store.loadCompletedAnswers(assessmentId);
    await store.enableSkillAreaRetake('fundamentals');

    // Verify state after async
    expect(store.getState().retakeAreas).toContain('fundamentals');

    // Continue flow
    await store.saveAnswer('q1', 4);

    // Verify final state
    expect(useStore.getState().userAnswers['q1']).toBe(4);
  });
});

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: 'AssessmentStore' }
  )
);

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

  saveAnswer: (questionId: string, value: number) => {
    console.log('[saveAnswer] Before:', {
      questionId,
      value,
      currentAnswers: get().answers,
      retakeAreas: get().retakeAreas,
    });

    set((state) => ({
      answers: { ...state.answers, [questionId]: value },
    }));

    console.log('[saveAnswer] After:', {
      answers: get().answers,
    });
  },
}));

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) => ({...}));