初心者がハマりやすい Zustand のエラーとその解決策まとめ

Zustand を使い始めた際に「なぜかエラーが出て動かない」「思った通りに動作しない」といった経験はありませんか?本記事では、初心者が特につまずきやすい Zustand のエラーパターンを体系的に整理し、実際のエラーメッセージとともに具体的な解決策をご紹介します。
エラーに遭遇した際の診断方法から予防策まで、実践的なデバッグテクニックを身につけて、スムーズな Zustand 開発を実現しましょう。
背景
初心者がエラーに遭遇しやすい理由
Zustand はシンプルで直感的な API を提供する状態管理ライブラリですが、その手軽さゆえに初心者が見落としがちな落とし穴が存在します。
特に以下のような特性により、予期しないエラーに遭遇することがあります。
# | 特性 | 初心者への影響 |
---|---|---|
1 | 最小限の設定で動作 | 内部動作の理解不足によるミス |
2 | TypeScript との高い親和性 | 型定義の誤りが複雑なエラーを生む |
3 | React の仕組みに依存 | React のライフサイクルとの競合 |
4 | 柔軟なカスタマイズ性 | 不適切な設計によるパフォーマンス問題 |
typescript// よくある初心者の実装例
const useStore = create((set) => ({
count: 0,
// ❌ 型情報が不足している
increment: () =>
set((state) => ({ count: state.count + 1 })),
}));
Zustand 特有のエラーパターン
他の状態管理ライブラリと比較して、Zustand では以下のようなエラーパターンが特徴的です。
Redux との違い Redux では action と reducer の分離により、エラーの発生箇所が明確になりやすいのに対し、Zustand ではストア内で直接状態を更新するため、エラーの原因特定が困難になることがあります。
typescript// Redux スタイル(エラー箇所が明確)
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }; // ここでエラーが発生
}
};
// Zustand スタイル(エラー箇所が不明確になりがち)
const useStore = create((set) => ({
count: 0,
increment: () =>
set((state) => ({
count: state.count + 1, // エラーがここなのか、setの呼び出しなのか判断しづらい
})),
}));
課題
エラー解決の難しさ
Zustand でのエラー解決が困難な理由として、以下の要因が挙げられます。
1. エラーメッセージの抽象性 多くの場合、Zustand 由来のエラーは React や TypeScript のエラーとして表示されるため、根本原因の特定が困難です。
bash# よく見るエラーメッセージの例
TypeError: Cannot read properties of undefined (reading 'count')
Type '(set: SetState<StoreState>) => StoreState' is not assignable to type 'StateCreator<StoreState, [], [], StoreState>'
2. 暗黙的な依存関係 Zustand は React の仕組みに深く依存しているため、React の理解不足がエラーの原因となることがあります。
3. デバッグツールの活用不足 Redux DevTools との連携方法や、適切なデバッグ手法を知らないことで、問題の特定に時間がかかります。
ドキュメント不足の問題
公式ドキュメントは基本的な使用方法に焦点を当てており、エラーハンドリングやトラブルシューティングの情報が限定的です。特に日本語での情報は少なく、初心者にとって解決策を見つけることが困難な状況です。
typescript// ドキュメントには載っていない、よくあるエラーケース
const useStore = create((set) => ({
data: null,
fetchData: async () => {
// ❌ 非同期処理中にコンポーネントがアンマウントされるとエラー
const response = await api.getData();
set({ data: response });
},
}));
解決策
コンパイルエラー編
TypeScript 型エラー
TypeScript を使用している場合に最も頻繁に遭遇するエラーパターンです。
エラー例 1: インターフェース定義の不整合
bashType '(set: SetState<unknown>) => { count: number; increment: () => void; }' is not assignable to type 'StateCreator<StoreState, [], [], StoreState>'.
原因と解決策:
typescript// ❌ 問題のあるコード
interface StoreState {
count: number;
increment: () => void;
}
const useStore = create<StoreState>((set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
// ❌ インターフェースにない不正なプロパティ
extraProperty: 'invalid',
}));
// ✅ 修正版
interface StoreState {
count: number;
increment: () => void;
}
const useStore = create<StoreState>((set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
}));
エラー例 2: ジェネリクス型の指定ミス
bashArgument of type '(set: SetState<StoreState>) => StoreState' is not assignable to parameter of type 'StateCreator<StoreState, [], [], StoreState>'.
解決策:
typescript// ❌ 問題のあるコード
const useStore = create<StoreState>((set, get) => ({
count: 0,
doubleCount: () => {
// ❌ get() の型が推論されない
return get().count * 2;
},
}));
// ✅ 修正版
const useStore = create<StoreState>()((set, get) => ({
count: 0,
doubleCount: () => {
// ✅ 正しく型推論される
return get().count * 2;
},
}));
インポートエラー
エラー例:モジュール解決の失敗
bashModule '"zustand"' has no exported member 'create'.
Cannot find module 'zustand' or its type declarations.
解決策:
typescript// ❌ 間違ったインポート
import { create } from 'zustand';
// ✅ 正しいインポート(v4以降)
import { create } from 'zustand';
// ✅ v3以前の場合
import create from 'zustand';
バージョンの確認とインストールの実行:
bash# パッケージバージョンの確認
yarn list zustand
# 最新版へのアップデート
yarn upgrade zustand
# 型定義の確認
yarn add -D @types/react
中間件との型エラー
エラー例:devtools との連携
bashType 'StateCreator<StoreState, [["zustand/devtools", never]], [], StoreState>' is not assignable to type 'StateCreator<StoreState, [], [], StoreState>'.
解決策:
typescript// ❌ 問題のあるコード
import { devtools } from 'zustand/middleware';
const useStore = create<StoreState>(
devtools((set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
}))
);
// ✅ 修正版
import { devtools } from 'zustand/middleware';
interface StoreState {
count: number;
increment: () => void;
}
const useStore = create<StoreState>()(
devtools(
(set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
}),
{
name: 'counter-store', // devtools での表示名
}
)
);
ランタイムエラー編
無限ループエラー
エラー例:
bashMaximum update depth exceeded. This can happen when a component repeatedly calls setState inside useEffect, or when a component calls setState inside its render function.
原因と解決策:
typescript// ❌ 問題のあるコード(無限ループを引き起こす)
const useStore = create((set) => ({
count: 0,
increment: () => {
// ❌ set内でsetを呼び出している
set((state) => {
set({ count: state.count + 1 });
return state;
});
},
}));
// コンポーネント内での使用
const Component = () => {
const { count, increment } = useStore();
// ❌ レンダリング中にstateを更新
increment();
return <div>{count}</div>;
};
// ✅ 修正版
const useStore = create((set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
}));
const Component = () => {
const { count, increment } = useStore();
// ✅ イベントハンドラ内で更新
const handleClick = () => {
increment();
};
return <button onClick={handleClick}>{count}</button>;
};
メモリリーク
症状: ページ遷移後もストアが残り続け、メモリ使用量が増加していく
原因と解決策:
typescript// ❌ 問題のあるコード
const useStore = create((set) => ({
timer: null as NodeJS.Timeout | null,
startTimer: () => {
// ❌ タイマーをクリアせずに新しいタイマーを開始
const timer = setInterval(() => {
console.log('Timer running...');
}, 1000);
set({ timer });
},
stopTimer: () => {
// ❌ タイマーの解放処理が不完全
set({ timer: null });
},
}));
// ✅ 修正版
const useStore = create((set, get) => ({
timer: null as NodeJS.Timeout | null,
startTimer: () => {
// ✅ 既存のタイマーをクリア
const { timer } = get();
if (timer) {
clearInterval(timer);
}
const newTimer = setInterval(() => {
console.log('Timer running...');
}, 1000);
set({ timer: newTimer });
},
stopTimer: () => {
// ✅ 適切なクリーンアップ
const { timer } = get();
if (timer) {
clearInterval(timer);
set({ timer: null });
}
},
}));
// コンポーネントでのクリーンアップ
const Component = () => {
const { startTimer, stopTimer } = useStore();
useEffect(() => {
startTimer();
// ✅ コンポーネントのアンマウント時にクリーンアップ
return () => {
stopTimer();
};
}, []);
return <div>Timer Component</div>;
};
状態更新の失敗
エラー例:
bashCannot read properties of null (reading 'someProperty')
TypeError: state.data is undefined
原因と解決策:
typescript// ❌ 問題のあるコード
const useStore = create((set) => ({
user: null,
updateUserName: (name) => {
// ❌ null チェックなしでプロパティにアクセス
set((state) => ({
user: {
...state.user,
name: name,
},
}));
},
}));
// ✅ 修正版
interface User {
id: string;
name: string;
email: string;
}
interface StoreState {
user: User | null;
updateUserName: (name: string) => void;
resetUser: () => void;
}
const useStore = create<StoreState>((set) => ({
user: null,
updateUserName: (name) => {
set((state) => {
// ✅ null チェックを実装
if (!state.user) {
console.warn(
'User is not set. Cannot update name.'
);
return state;
}
return {
user: {
...state.user,
name: name,
},
};
});
},
resetUser: () => set({ user: null }),
}));
パフォーマンス問題編
不要な再レンダリング
症状: コンポーネントが予期せず頻繁に再レンダリングされる
原因と解決策:
typescript// ❌ 問題のあるコード
const Component = () => {
// ❌ ストア全体を取得するため、関係ない更新でも再レンダリング
const store = useStore();
return <div>{store.count}</div>;
};
// ✅ 修正版 1: 必要な値のみを選択
const Component = () => {
const count = useStore((state) => state.count);
return <div>{count}</div>;
};
// ✅ 修正版 2: 複数の値を効率的に取得
const Component = () => {
const { count, isLoading } = useStore(
(state) => ({
count: state.count,
isLoading: state.isLoading,
}),
shallow // shallow比較を使用
);
return <div>{isLoading ? 'Loading...' : count}</div>;
};
セレクタの誤用
問題のあるセレクタの例:
typescript// ❌ 問題のあるコード
const Component = () => {
// ❌ 毎回新しいオブジェクトを返すため、常に再レンダリング
const derivedData = useStore((state) => ({
doubled: state.count * 2,
isEven: state.count % 2 === 0,
}));
return <div>{derivedData.doubled}</div>;
};
// ✅ 修正版 1: useMemo を活用
const Component = () => {
const count = useStore((state) => state.count);
const derivedData = useMemo(
() => ({
doubled: count * 2,
isEven: count % 2 === 0,
}),
[count]
);
return <div>{derivedData.doubled}</div>;
};
// ✅ 修正版 2: ストア内で計算済みの値を提供
const useStore = create((set, get) => ({
count: 0,
get doubled() {
return get().count * 2;
},
get isEven() {
return get().count % 2 === 0;
},
increment: () =>
set((state) => ({ count: state.count + 1 })),
}));
shallow 比較の問題
エラー例:
bashWarning: Maximum update depth exceeded
原因と解決策:
typescriptimport { shallow } from 'zustand/shallow';
// ❌ 問題のあるコード
const Component = () => {
// ❌ shallow を使わずに複数の値を取得
const { users, filters } = useStore((state) => ({
users: state.users,
filters: state.filters,
}));
// users や filters が更新されるたびに新しいオブジェクトが生成され、
// 関連するコンポーネントが不要に再レンダリングされる
return <UserList users={users} filters={filters} />;
};
// ✅ 修正版
const Component = () => {
const { users, filters } = useStore(
(state) => ({
users: state.users,
filters: state.filters,
}),
shallow // shallow比較により、実際の値が変更された場合のみ再レンダリング
);
return <UserList users={users} filters={filters} />;
};
設計・実装ミス編
アンチパターン
1. ストアの過度な分割
typescript// ❌ アンチパターン:関連する状態を不必要に分割
const useUserStore = create((set) => ({
name: '',
setName: (name) => set({ name }),
}));
const useUserEmailStore = create((set) => ({
email: '',
setEmail: (email) => set({ email }),
}));
const useUserAgeStore = create((set) => ({
age: 0,
setAge: (age) => set({ age }),
}));
// ✅ 改善版:関連する状態をまとめて管理
const useUserStore = create((set) => ({
profile: {
name: '',
email: '',
age: 0,
},
updateProfile: (updates) =>
set((state) => ({
profile: { ...state.profile, ...updates },
})),
}));
2. 状態とロジックの混在
typescript// ❌ アンチパターン:ビジネスロジックがストアに混在
const useStore = create((set, get) => ({
products: [],
cart: [],
user: null,
// ❌ 複雑なビジネスロジックがストア内に
processCheckout: async () => {
const { cart, user } = get();
// 長大な処理...
if (!user) throw new Error('User not logged in');
if (cart.length === 0) throw new Error('Cart is empty');
const total = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const tax = total * 0.1;
try {
const response = await api.processPayment({
userId: user.id,
items: cart,
total: total + tax,
});
if (response.success) {
set({ cart: [] });
// さらに長い処理...
}
} catch (error) {
// エラーハンドリング...
}
},
}));
// ✅ 改善版:ビジネスロジックを別の層に分離
class CheckoutService {
static async processCheckout(cart, user) {
if (!user) throw new Error('User not logged in');
if (cart.length === 0) throw new Error('Cart is empty');
const total = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const tax = total * 0.1;
return await api.processPayment({
userId: user.id,
items: cart,
total: total + tax,
});
}
}
const useStore = create((set, get) => ({
products: [],
cart: [],
user: null,
isProcessingCheckout: false,
processCheckout: async () => {
set({ isProcessingCheckout: true });
try {
const { cart, user } = get();
await CheckoutService.processCheckout(cart, user);
set({ cart: [], isProcessingCheckout: false });
} catch (error) {
set({ isProcessingCheckout: false });
throw error;
}
},
}));
状態の分散
問題:関連する状態が複数のストアに分散している
typescript// ❌ 問題のあるコード:関連する状態が分散
const useAuthStore = create((set) => ({
isAuthenticated: false,
login: () => set({ isAuthenticated: true }),
}));
const useUserStore = create((set) => ({
userData: null,
setUserData: (data) => set({ userData: data }),
}));
const usePermissionStore = create((set) => ({
permissions: [],
setPermissions: (perms) => set({ permissions: perms }),
}));
// コンポーネントで複数のストアを監視する必要がある
const Header = () => {
const isAuthenticated = useAuthStore(
(state) => state.isAuthenticated
);
const userData = useUserStore((state) => state.userData);
const permissions = usePermissionStore(
(state) => state.permissions
);
// 3つのストアの同期を手動で管理する必要がある
return <div>...</div>;
};
// ✅ 改善版:関連する状態をまとめて管理
interface AuthState {
isAuthenticated: boolean;
userData: UserData | null;
permissions: Permission[];
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthState>((set) => ({
isAuthenticated: false,
userData: null,
permissions: [],
login: async (credentials) => {
try {
const response = await authAPI.login(credentials);
set({
isAuthenticated: true,
userData: response.user,
permissions: response.permissions,
});
} catch (error) {
throw error;
}
},
logout: () => {
set({
isAuthenticated: false,
userData: null,
permissions: [],
});
},
}));
適切でない初期化
問題:非同期初期化の不適切な実装
typescript// ❌ 問題のあるコード
const useStore = create((set) => {
// ❌ 作成時に非同期処理を実行(エラーハンドリングなし)
loadInitialData().then((data) => {
set({ data });
});
return {
data: null,
isLoading: true, // ❌ 初期化の完了を追跡できない
};
});
// ✅ 改善版
interface StoreState {
data: any | null;
isLoading: boolean;
error: string | null;
initialize: () => Promise<void>;
}
const useStore = create<StoreState>((set) => ({
data: null,
isLoading: false,
error: null,
initialize: async () => {
set({ isLoading: true, error: null });
try {
const data = await loadInitialData();
set({ data, isLoading: false });
} catch (error) {
set({
error: error.message,
isLoading: false,
});
}
},
}));
// コンポーネントでの適切な初期化
const App = () => {
const { initialize, isLoading, error } = useStore();
useEffect(() => {
initialize();
}, []);
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return <MainContent />;
};
具体例
実際のエラーメッセージと解決手順
ケース 1: セレクタ関数での TypeScript エラー
エラーメッセージ:
bashArgument of type '(state: unknown) => any' is not assignable to parameter of type '(state: StoreState) => any'.
Parameter 'state' is implicitly has an 'any' type.
問題のコード:
typescriptinterface StoreState {
count: number;
name: string;
}
const useStore = create<StoreState>((set) => ({
count: 0,
name: '',
updateCount: (value) => set({ count: value }),
}));
// ❌ 型推論が働かない
const Component = () => {
const count = useStore((state) => state.count);
return <div>{count}</div>;
};
解決手順:
- 型定義の確認
typescript// ストアの型定義を明示的に指定
const useStore = create<StoreState>()((set) => ({
count: 0,
name: '',
updateCount: (value: number) => set({ count: value }),
}));
- セレクタの型指定
typescriptconst Component = () => {
const count = useStore(
(state: StoreState) => state.count
);
return <div>{count}</div>;
};
- 型安全なカスタムフック(推奨)
typescriptconst useStoreCount = () =>
useStore((state) => state.count);
const Component = () => {
const count = useStoreCount();
return <div>{count}</div>;
};
ケース 2: 非同期処理での状態更新エラー
エラーメッセージ:
bashWarning: Can't perform a React state update on an unmounted component.
問題のコード:
typescriptconst useStore = create((set) => ({
data: null,
loading: false,
fetchData: async () => {
set({ loading: true });
try {
// ❌ コンポーネントがアンマウントされた後に実行される可能性
const response = await api.getData();
set({ data: response, loading: false });
} catch (error) {
set({ loading: false });
}
},
}));
解決手順:
- AbortController の導入
typescriptconst useStore = create((set, get) => ({
data: null,
loading: false,
abortController: null,
fetchData: async () => {
// 既存のリクエストをキャンセル
const currentController = get().abortController;
if (currentController) {
currentController.abort();
}
const newController = new AbortController();
set({ loading: true, abortController: newController });
try {
const response = await api.getData({
signal: newController.signal,
});
// ✅ キャンセルされていない場合のみ状態を更新
if (!newController.signal.aborted) {
set({ data: response, loading: false });
}
} catch (error) {
if (!newController.signal.aborted) {
set({ loading: false });
}
}
},
cleanup: () => {
const controller = get().abortController;
if (controller) {
controller.abort();
}
},
}));
- コンポーネントでのクリーンアップ
typescriptconst DataComponent = () => {
const { fetchData, cleanup, data, loading } = useStore();
useEffect(() => {
fetchData();
// ✅ アンマウント時にクリーンアップ
return () => {
cleanup();
};
}, []);
if (loading) return <div>読み込み中...</div>;
return <div>{JSON.stringify(data)}</div>;
};
ケース 3: パフォーマンス問題の解決
症状:リストコンポーネントの重い再レンダリング
問題のコード:
typescriptconst useStore = create((set) => ({
items: [],
selectedId: null,
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
})),
selectItem: (id) => set({ selectedId: id }),
}));
// ❌ 全てのアイテムが再レンダリングされる
const ItemList = () => {
const { items, selectedId } = useStore();
return (
<div>
{items.map((item) => (
<ItemComponent
key={item.id}
item={item}
isSelected={selectedId === item.id}
/>
))}
</div>
);
};
解決手順:
- メモ化の導入
typescriptconst ItemComponent = React.memo(({ item, isSelected }) => {
console.log(`Rendering item ${item.id}`);
return (
<div className={isSelected ? 'selected' : ''}>
{item.name}
</div>
);
});
- セレクタの最適化
typescriptconst useItem = (itemId) => {
return useStore(
(state) => ({
item: state.items.find((item) => item.id === itemId),
isSelected: state.selectedId === itemId,
}),
shallow
);
};
const ItemComponent = ({ itemId }) => {
const { item, isSelected } = useItem(itemId);
if (!item) return null;
return (
<div className={isSelected ? 'selected' : ''}>
{item.name}
</div>
);
};
const ItemList = () => {
const items = useStore((state) => state.items);
return (
<div>
{items.map((item) => (
<ItemComponent key={item.id} itemId={item.id} />
))}
</div>
);
};
- ストア設計の見直し
typescript// アイテムをIDでインデックス化
const useStore = create((set) => ({
itemsById: {},
itemIds: [],
selectedId: null,
addItem: (item) =>
set((state) => ({
itemsById: { ...state.itemsById, [item.id]: item },
itemIds: [...state.itemIds, item.id],
})),
updateItem: (id, updates) =>
set((state) => ({
itemsById: {
...state.itemsById,
[id]: { ...state.itemsById[id], ...updates },
},
})),
selectItem: (id) => set({ selectedId: id }),
}));
まとめ
Zustand でのエラー解決は、エラーの種類を正しく分類し、それぞれに適した対処法を適用することが重要です。本記事でご紹介した内容をまとめると、以下のようになります。
エラー予防のベストプラクティス
# | カテゴリ | ベストプラクティス |
---|---|---|
1 | 型安全性 | TypeScript の厳密な型定義とジェネリクス活用 |
2 | パフォーマンス | 適切なセレクタと shallow 比較の使用 |
3 | 非同期処理 | AbortController とクリーンアップの実装 |
4 | ストア設計 | 責任の分離と適切な粒度での状態管理 |
5 | デバッグ | Redux DevTools の活用と段階的なテスト実装 |
開発時のチェックポイント
1. 実装前のチェック
- ストアの責任範囲は適切か?
- 型定義は十分に厳密か?
- 非同期処理のライフサイクルを考慮しているか?
2. 実装中のチェック
- セレクタは最小限の値のみを取得しているか?
- 不要な再レンダリングが発生していないか?
- エラーハンドリングは適切に実装されているか?
3. テスト・デバッグ時のチェック
- Redux DevTools でストアの状態変化を確認
- React Developer Tools でレンダリング回数をチェック
- メモリリークがないかパフォーマンス監視
効果的なデバッグツール
typescript// DevTools の設定
const useStore = create<StoreState>()(
devtools(
(set) => ({
// ストア実装
}),
{
name: 'my-store',
trace: true, // スタックトレースを表示
serialize: true, // シリアライゼーションを有効化
}
)
);
// ログミドルウェアの実装
const logMiddleware = (config) => (set, get, api) =>
config(
(...args) => {
console.log(' applying', args);
set(...args);
console.log(' new state', get());
},
get,
api
);
const useStore = create(
logMiddleware((set) => ({
// ストア実装
}))
);
これらのベストプラクティスを実践することで、Zustand でのエラーを効果的に予防し、発生した場合も迅速に解決できるようになります。エラーに遭遇した際は、まずエラーの分類を行い、本記事で紹介した解決パターンを参考に対処してみてください。
関連リンク
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体
- review
瞬時に答えが出る脳に変身!『ゼロ秒思考』赤羽雄二が贈る思考力爆上げトレーニング
- review
関西弁のゾウに人生変えられた!『夢をかなえるゾウ 1』水野敬也が教えてくれた成功の本質
- review
「なぜ私の考えは浅いのか?」の答えがここに『「具体 ⇄ 抽象」トレーニング』細谷功
- review
もうプレーヤー思考は卒業!『リーダーの仮面』安藤広大で掴んだマネジャー成功の極意