Zustand Devtools の使い方とデバッグを楽にする活用術

Zustand を使った開発で、「なぜ状態が更新されないのか?」「どこでバグが発生しているのか?」といった疑問を抱いたことはありませんか?フロントエンド開発において、状態管理のデバッグは避けて通れない重要なスキルです。今回は、Zustand Devtools を活用して開発効率を劇的に向上させる方法をご紹介します。実際の開発現場で使える実践的なテクニックから、チーム開発での活用方法まで、幅広くカバーしていきますね。
Devtools 環境構築
Redux DevTools Extension のインストール
Zustand Devtools は Redux DevTools Extension を基盤としているため、まずはブラウザ拡張機能のインストールが必要です。
# | ブラウザ | インストール方法 |
---|---|---|
1 | Chrome | Chrome Web Store で「Redux DevTools」を検索してインストール |
2 | Firefox | Firefox Add-ons で「Redux DevTools」を検索してインストール |
3 | Edge | Microsoft Store で「Redux DevTools」を検索してインストール |
4 | Safari | App Store で「Redux DevTools」を検索してインストール |
インストール後、ブラウザの開発者ツールを開くと、新しく「Redux」タブが表示されます。これが Zustand Devtools の操作パネルになります。
Zustand での Devtools 設定
次に、Zustand ストアで Devtools を有効化する設定を行います。
typescriptimport { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface UserState {
users: User[];
currentUser: User | null;
isLoading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
setCurrentUser: (user: User) => void;
clearError: () => void;
}
interface User {
id: number;
name: string;
email: string;
}
const useUserStore = create<UserState>()(
devtools(
(set, get) => ({
users: [],
currentUser: null,
isLoading: false,
error: null,
fetchUsers: async () => {
set(
{ isLoading: true, error: null },
false,
'fetchUsers/start'
);
try {
const response = await fetch('/api/users');
const users = await response.json();
set(
{ users, isLoading: false },
false,
'fetchUsers/success'
);
} catch (error) {
set(
{
error: error.message,
isLoading: false,
},
false,
'fetchUsers/error'
);
}
},
setCurrentUser: (user) => {
set({ currentUser: user }, false, 'setCurrentUser');
},
clearError: () => {
set({ error: null }, false, 'clearError');
},
}),
{
name: 'user-store', // Devtools上での表示名
enabled: process.env.NODE_ENV === 'development', // 開発環境でのみ有効
}
)
);
ポイントはset
の第 3 引数でアクション名を指定することです。これにより、Devtools 上でどのアクションが実行されたかが明確になります。
開発環境とプロダクション環境の使い分け
本番環境でのパフォーマンス低下を避けるため、環境に応じた設定を行いましょう。
typescript// 環境変数を活用した条件分岐
const createUserStore = () => {
const storeConfig = (set, get) => ({
// ストアの実装
});
if (process.env.NODE_ENV === 'development') {
return create(
devtools(storeConfig, {
name: 'user-store',
enabled: true,
serialize: true, // 複雑なオブジェクトもシリアライズ
trace: true, // スタックトレースを含める
})
);
}
return create(storeConfig);
};
export const useUserStore = createUserStore();
より細かい制御が必要な場合は、以下のような設定も可能です。
typescript// カスタム設定での詳細制御
const devtoolsConfig = {
name: 'user-store',
enabled: process.env.NODE_ENV === 'development',
anonymousActionType: 'unknown', // 未指定アクションの表示名
serialize: {
options: {
undefined: true,
function: true,
symbol: true,
},
},
};
const useUserStore = create<UserState>()(
devtools(storeImplementation, devtoolsConfig)
);
基本的なデバッグ機能
State の可視化とリアルタイム監視
Devtools を開くと、現在のストア状態がリアルタイムで表示されます。状態の変化を監視する際の効果的な使い方をご紹介します。
typescript// デバッグしやすい状態構造の設計
interface AppState {
// セクションごとに状態を分離
ui: {
isModalOpen: boolean;
selectedTab: string;
notifications: Notification[];
};
data: {
users: User[];
posts: Post[];
comments: Comment[];
};
async: {
loading: {
users: boolean;
posts: boolean;
comments: boolean;
};
errors: {
users: string | null;
posts: string | null;
comments: string | null;
};
};
}
このような構造にすることで、Devtools 上での状態確認が格段に楽になります。
Action の履歴追跡
アクション履歴を効果的に活用するため、意味のあるアクション名を設定します。
typescriptconst useShoppingCartStore = create<ShoppingCartState>()(
devtools(
(set, get) => ({
items: [],
total: 0,
addItem: (product: Product, quantity: number) => {
const currentItems = get().items;
const existingItem = currentItems.find(
(item) => item.id === product.id
);
if (existingItem) {
set(
(state) => ({
items: state.items.map((item) =>
item.id === product.id
? {
...item,
quantity: item.quantity + quantity,
}
: item
),
}),
false,
`addItem/${product.name}/quantity:${quantity}` // 詳細なアクション名
);
} else {
set(
(state) => ({
items: [
...state.items,
{ ...product, quantity },
],
}),
false,
`addItem/${product.name}/new`
);
}
// 合計金額の更新
set(
(state) => ({
total: state.items.reduce(
(sum, item) =>
sum + item.price * item.quantity,
0
),
}),
false,
'updateTotal'
);
},
}),
{ name: 'shopping-cart' }
)
);
Time Travel デバッグの活用
Time Travel デバッグは、過去の状態に戻って問題を特定する強力な機能です。
typescript// Time Travel デバッグに適した状態管理
const useFormStore = create<FormState>()(
devtools(
(set, get) => ({
formData: {
name: '',
email: '',
phone: '',
},
validationErrors: {},
isSubmitting: false,
updateField: (
field: keyof FormData,
value: string
) => {
set(
(state) => ({
formData: {
...state.formData,
[field]: value,
},
// バリデーションエラーをクリア
validationErrors: {
...state.validationErrors,
[field]: null,
},
}),
false,
`updateField/${field}` // フィールド単位でのアクション追跡
);
},
validateForm: () => {
const { formData } = get();
const errors: ValidationErrors = {};
if (!formData.name.trim()) {
errors.name = '名前は必須です';
}
if (!formData.email.includes('@')) {
errors.email =
'有効なメールアドレスを入力してください';
}
set(
{ validationErrors: errors },
false,
'validateForm'
);
return Object.keys(errors).length === 0;
},
}),
{ name: 'form-store' }
)
);
高度なデバッグテクニック
State の差分比較とパフォーマンス監視
複雑な状態変更の際は、差分比較機能を活用します。
typescript// パフォーマンス監視用のカスタムミドルウェア
const performanceLogger = (config) => (set, get, api) =>
config(
(...args) => {
const startTime = performance.now();
const result = set(...args);
const endTime = performance.now();
if (endTime - startTime > 5) {
// 5ms以上かかった場合に警告
console.warn(
`Slow state update: ${endTime - startTime}ms`,
args
);
}
return result;
},
get,
api
);
const useOptimizedStore = create(
devtools(
performanceLogger((set, get) => ({
// ストアの実装
largeDataSet: [],
processLargeData: (data: LargeData[]) => {
// 大量データの処理
const processedData = data.map((item) => ({
...item,
processed: true,
timestamp: Date.now(),
}));
set(
{ largeDataSet: processedData },
false,
`processLargeData/count:${data.length}`
);
},
})),
{ name: 'optimized-store' }
)
);
カスタムアクション名の設定
動的なアクション名でより詳細な追跡を行います。
typescriptconst useNotificationStore = create<NotificationState>()(
devtools(
(set, get) => ({
notifications: [],
addNotification: (
type: NotificationType,
message: string,
duration?: number
) => {
const id = Date.now().toString();
const notification: Notification = {
id,
type,
message,
timestamp: new Date(),
duration: duration || 5000,
};
set(
(state) => ({
notifications: [
...state.notifications,
notification,
],
}),
false,
`addNotification/${type}/${id}` // タイプとIDを含む詳細な名前
);
// 自動削除の設定
if (notification.duration > 0) {
setTimeout(() => {
set(
(state) => ({
notifications: state.notifications.filter(
(n) => n.id !== id
),
}),
false,
`removeNotification/auto/${id}`
);
}, notification.duration);
}
},
removeNotification: (id: string) => {
set(
(state) => ({
notifications: state.notifications.filter(
(n) => n.id !== id
),
}),
false,
`removeNotification/manual/${id}`
);
},
}),
{ name: 'notification-store' }
)
);
複数ストアの同時監視
複数のストアを効率的に監視する方法をご紹介します。
typescript// ストア間の連携を監視しやすくする命名規則
const useAuthStore = create<AuthState>()(
devtools(
(set, get) => ({
user: null,
isAuthenticated: false,
login: async (credentials: Credentials) => {
set({ isLoading: true }, false, 'auth/login/start');
try {
const user = await authAPI.login(credentials);
set(
{
user,
isAuthenticated: true,
isLoading: false,
},
false,
'auth/login/success'
);
// 他のストアに影響を与える場合は明示的に記録
console.log(
'Auth success - triggering data fetch'
);
} catch (error) {
set(
{ error: error.message, isLoading: false },
false,
'auth/login/error'
);
}
},
}),
{ name: 'auth-store' }
)
);
const useDataStore = create<DataState>()(
devtools(
(set, get) => ({
userData: null,
fetchUserData: async () => {
const authState = useAuthStore.getState();
if (!authState.isAuthenticated) {
console.log(
'Skipping data fetch - user not authenticated'
);
return;
}
set({ isLoading: true }, false, 'data/fetch/start');
try {
const userData = await dataAPI.fetchUserData();
set(
{ userData, isLoading: false },
false,
'data/fetch/success'
);
} catch (error) {
set(
{ error: error.message, isLoading: false },
false,
'data/fetch/error'
);
}
},
}),
{ name: 'data-store' }
)
);
実際のデバッグシーン
フォーム入力時の状態変化追跡
実際のフォーム操作でのデバッグ例をご紹介します。
typescript// リアルタイムバリデーション付きフォームストア
const useFormStore = create<FormState>()(
devtools(
(set, get) => ({
fields: {
email: { value: '', error: null, touched: false },
password: {
value: '',
error: null,
touched: false,
},
confirmPassword: {
value: '',
error: null,
touched: false,
},
},
isValid: false,
updateField: (fieldName: string, value: string) => {
const currentState = get();
set(
(state) => ({
fields: {
...state.fields,
[fieldName]: {
...state.fields[fieldName],
value,
touched: true,
},
},
}),
false,
`updateField/${fieldName}/value:${value.substring(
0,
10
)}` // 値の一部を表示
);
// リアルタイムバリデーション
setTimeout(() => {
get().validateField(fieldName);
}, 300); // デバウンス
},
validateField: (fieldName: string) => {
const { fields } = get();
const field = fields[fieldName];
let error: string | null = null;
switch (fieldName) {
case 'email':
if (!field.value.includes('@')) {
error =
'メールアドレスの形式が正しくありません';
}
break;
case 'password':
if (field.value.length < 8) {
error =
'パスワードは8文字以上で入力してください';
}
break;
case 'confirmPassword':
if (field.value !== fields.password.value) {
error = 'パスワードが一致しません';
}
break;
}
set(
(state) => ({
fields: {
...state.fields,
[fieldName]: {
...state.fields[fieldName],
error,
},
},
}),
false,
`validateField/${fieldName}/result:${
error ? 'error' : 'valid'
}`
);
// フォーム全体の妥当性を更新
const updatedFields = {
...get().fields,
[fieldName]: { ...field, error },
};
const isValid = Object.values(updatedFields).every(
(f) => !f.error && f.touched
);
set(
{ isValid },
false,
`updateFormValidity/${isValid}`
);
},
}),
{ name: 'form-store' }
)
);
API 通信エラーのデバッグ
API 通信でのエラーハンドリングとデバッグ方法です。
typescript// 詳細なエラー情報を記録するストア
const useApiStore = create<ApiState>()(
devtools(
(set, get) => ({
requests: new Map(),
errors: [],
makeRequest: async <T>(
url: string,
options: RequestOptions = {}
): Promise<T | null> => {
const requestId = `${Date.now()}-${Math.random()}`;
// リクエスト開始をログ
set(
(state) => ({
requests: new Map(state.requests).set(
requestId,
{
url,
status: 'pending',
startTime: Date.now(),
}
),
}),
false,
`api/request/start/${url}/${requestId}`
);
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}
const data = await response.json();
// 成功をログ
set(
(state) => ({
requests: new Map(state.requests).set(
requestId,
{
url,
status: 'success',
startTime:
state.requests.get(requestId)
?.startTime || Date.now(),
endTime: Date.now(),
data,
}
),
}),
false,
`api/request/success/${url}/${requestId}`
);
return data;
} catch (error) {
const errorInfo = {
id: requestId,
url,
message: error.message,
timestamp: new Date(),
stack: error.stack,
};
// エラーをログ
set(
(state) => ({
requests: new Map(state.requests).set(
requestId,
{
url,
status: 'error',
startTime:
state.requests.get(requestId)
?.startTime || Date.now(),
endTime: Date.now(),
error: errorInfo,
}
),
errors: [...state.errors, errorInfo],
}),
false,
`api/request/error/${url}/${error.message}/${requestId}`
);
return null;
}
},
}),
{ name: 'api-store' }
)
);
非同期処理の状態管理
複雑な非同期処理のデバッグ方法をご紹介します。
typescript// 並列処理を含む非同期操作のデバッグ
const useAsyncStore = create<AsyncState>()(
devtools(
(set, get) => ({
tasks: new Map(),
results: new Map(),
executeParallelTasks: async (
taskConfigs: TaskConfig[]
) => {
const batchId = `batch-${Date.now()}`;
set(
(state) => ({
tasks: new Map(state.tasks).set(batchId, {
status: 'started',
taskCount: taskConfigs.length,
completedCount: 0,
startTime: Date.now(),
}),
}),
false,
`async/batch/start/${batchId}/tasks:${taskConfigs.length}`
);
const taskPromises = taskConfigs.map(
async (config, index) => {
const taskId = `${batchId}-task-${index}`;
try {
set(
(state) => {
const batch = state.tasks.get(batchId);
return {
tasks: new Map(state.tasks).set(
batchId,
{
...batch,
[`task-${index}`]: 'running',
}
),
};
},
false,
`async/task/start/${taskId}`
);
const result = await config.execute();
set(
(state) => {
const batch = state.tasks.get(batchId);
return {
tasks: new Map(state.tasks).set(
batchId,
{
...batch,
[`task-${index}`]: 'completed',
completedCount:
batch.completedCount + 1,
}
),
results: new Map(state.results).set(
taskId,
result
),
};
},
false,
`async/task/success/${taskId}`
);
return { taskId, result, error: null };
} catch (error) {
set(
(state) => {
const batch = state.tasks.get(batchId);
return {
tasks: new Map(state.tasks).set(
batchId,
{
...batch,
[`task-${index}`]: 'error',
completedCount:
batch.completedCount + 1,
}
),
};
},
false,
`async/task/error/${taskId}/${error.message}`
);
return { taskId, result: null, error };
}
}
);
const results = await Promise.allSettled(
taskPromises
);
set(
(state) => {
const batch = state.tasks.get(batchId);
return {
tasks: new Map(state.tasks).set(batchId, {
...batch,
status: 'completed',
endTime: Date.now(),
}),
};
},
false,
`async/batch/complete/${batchId}`
);
return results;
},
}),
{ name: 'async-store' }
)
);
チーム開発での活用術
デバッグ情報の共有方法
チーム間でのデバッグ情報共有を効率化する方法をご紹介します。
typescript// デバッグ情報エクスポート機能付きストア
const useDebugStore = create<DebugState>()(
devtools(
(set, get) => ({
debugMode: process.env.NODE_ENV === 'development',
sessionId: crypto.randomUUID(),
actionHistory: [],
exportDebugInfo: () => {
const state = get();
const debugInfo = {
sessionId: state.sessionId,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
actionHistory: state.actionHistory.slice(-50), // 直近50アクション
currentState: {
// 機密情報を除外した状態
...state,
actionHistory: undefined,
sensitiveData: '[REDACTED]',
},
};
// クリップボードにコピー
navigator.clipboard.writeText(
JSON.stringify(debugInfo, null, 2)
);
set(
{ lastExport: Date.now() },
false,
'debug/export/clipboard'
);
return debugInfo;
},
logAction: (action: string, payload?: any) => {
if (!get().debugMode) return;
const logEntry = {
timestamp: Date.now(),
action,
payload: payload
? JSON.parse(JSON.stringify(payload))
: null,
url: window.location.pathname,
};
set(
(state) => ({
actionHistory: [
...state.actionHistory.slice(-99),
logEntry,
], // 最大100件保持
}),
false,
'debug/log/action'
);
},
}),
{ name: 'debug-store' }
)
);
// 使用例:他のストアでのアクションログ
const useUserStore = create<UserState>()(
devtools(
(set, get) => ({
// ... 他の状態
updateProfile: async (profileData: ProfileData) => {
// デバッグログの記録
useDebugStore
.getState()
.logAction('user/updateProfile/start', {
userId: get().currentUser?.id,
fields: Object.keys(profileData),
});
try {
const result = await userAPI.updateProfile(
profileData
);
set(
{ currentUser: result },
false,
'user/updateProfile/success'
);
useDebugStore
.getState()
.logAction('user/updateProfile/success', {
userId: result.id,
});
} catch (error) {
useDebugStore
.getState()
.logAction('user/updateProfile/error', {
error: error.message,
});
throw error;
}
},
}),
{ name: 'user-store' }
)
);
バグレポートでの Devtools 活用
効果的なバグレポート作成のための Devtools 活用法です。
typescript// バグレポート生成機能
const useBugReportStore = create<BugReportState>()(
devtools(
(set, get) => ({
reports: [],
generateBugReport: (
description: string,
steps: string[]
) => {
const report: BugReport = {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
description,
steps,
environment: {
userAgent: navigator.userAgent,
url: window.location.href,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
timestamp: Date.now(),
},
// 各ストアの現在状態をスナップショット
storeSnapshots: {
user: useUserStore.getState(),
ui: useUIStore.getState(),
data: useDataStore.getState(),
},
// Devtoolsから最近のアクション履歴を取得
recentActions: getRecentActions(), // 後述の実装
console: getRecentConsoleMessages(), // コンソールログも含める
};
set(
(state) => ({
reports: [...state.reports, report],
}),
false,
`bugReport/create/${report.id}`
);
return report;
},
exportReport: (reportId: string) => {
const report = get().reports.find(
(r) => r.id === reportId
);
if (!report) return null;
const exportData = {
...report,
// 機密情報の除外
storeSnapshots: Object.fromEntries(
Object.entries(report.storeSnapshots).map(
([key, value]) => [
key,
sanitizeForExport(value),
]
)
),
};
// JSONファイルとしてダウンロード
const blob = new Blob(
[JSON.stringify(exportData, null, 2)],
{
type: 'application/json',
}
);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bug-report-${reportId.slice(
0,
8
)}.json`;
a.click();
URL.revokeObjectURL(url);
set(
{ lastExport: Date.now() },
false,
`bugReport/export/${reportId}`
);
return exportData;
},
}),
{ name: 'bug-report-store' }
)
);
// ヘルパー関数
function getRecentActions(): ActionHistory[] {
// Redux DevTools APIを使用してアクション履歴を取得
if (
typeof window !== 'undefined' &&
window.__REDUX_DEVTOOLS_EXTENSION__
) {
const devtools = window.__REDUX_DEVTOOLS_EXTENSION__;
// 実際の実装では、DevTools APIを使用してアクション履歴を取得
return [];
}
return [];
}
function getRecentConsoleMessages(): ConsoleMessage[] {
// コンソールメッセージの履歴を取得(カスタム実装が必要)
return [];
}
function sanitizeForExport(data: any): any {
// 機密情報を除外する処理
const sanitized = JSON.parse(JSON.stringify(data));
// 一般的な機密フィールドを除外
const sensitiveFields = [
'password',
'token',
'secret',
'key',
'auth',
];
function recursiveSanitize(obj: any): any {
if (typeof obj !== 'object' || obj === null) return obj;
const result = Array.isArray(obj) ? [] : {};
for (const [key, value] of Object.entries(obj)) {
if (
sensitiveFields.some((field) =>
key.toLowerCase().includes(field)
)
) {
result[key] = '[REDACTED]';
} else if (typeof value === 'object') {
result[key] = recursiveSanitize(value);
} else {
result[key] = value;
}
}
return result;
}
return recursiveSanitize(sanitized);
}
まとめ
Zustand Devtools は、単なるデバッグツール以上の価値を持つ強力な開発支援ツールです。今回ご紹介した活用術を実践することで、以下のような効果が期待できます。
開発効率の向上: リアルタイムでの状態監視により、問題の早期発見と迅速な修正が可能になります。特に、アクション履歴と Time Travel デバッグを組み合わせることで、バグの根本原因を素早く特定できるでしょう。
チーム開発の円滑化: 統一されたデバッグ情報の記録と共有により、チームメンバー間でのコミュニケーションが改善されます。バグレポート機能を活用することで、再現可能な詳細な情報を提供できますね。
コードの品質向上: パフォーマンス監視機能を使うことで、状態更新の最適化ポイントが明確になります。また、構造化されたアクション名により、コードの可読性も向上します。
Devtools は開発フェーズだけでなく、テスト段階や本番環境での問題調査にも威力を発揮します。ぜひ、今日から実際のプロジェクトで活用してみてください。最初は設定に時間がかかるかもしれませんが、長期的に見れば開発時間の大幅な短縮につながることでしょう。
次のステップとして、プロジェクト固有のカスタムミドルウェアの開発や、CI/CD パイプラインでの Devtools 活用なども検討してみてくださいね。
関連リンク
- review
もう「なんとなく」で決めない!『解像度を上げる』馬田隆明著で身につけた、曖昧思考を一瞬で明晰にする技術
- review
もう疲れ知らず!『最高の体調』鈴木祐著で手に入れた、一生モノの健康習慣術
- review
人生が激変!『苦しかったときの話をしようか』森岡毅著で発見した、本当に幸せなキャリアの築き方
- review
もう「何言ってるの?」とは言わせない!『バナナの魅力を 100 文字で伝えてください』柿内尚文著 で今日からあなたも伝え方の達人!
- review
もう時間に追われない!『エッセンシャル思考』グレッグ・マキューンで本当に重要なことを見抜く!
- review
プロダクト開発の悩みを一刀両断!『プロダクトマネジメントのすべて』及川 卓也, 曽根原 春樹, 小城 久美子