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.
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
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
$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. 下の青いボタンを押して
rn-zustand-patterns.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
rn-zustand-patternsフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
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 synchronouslygetState()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) => ({...}));