Zustand の状態初期化とリセット処理のパターンまとめ

Zustand で状態管理を行う際、最も重要な課題の一つが状態の初期化とリセット処理です。適切な状態リセットが実装されていないと、ユーザーが予期しない動作に遭遇したり、アプリケーションの状態が不整合に陥ったりする可能性があります。
特に、ユーザーがログアウトした後に再度ログインした際、前回のセッション情報が残ってしまう問題や、フォームの入力内容が意図せず保持されてしまう問題は、多くの開発者が経験しているのではないでしょうか。
この記事では、Zustand での状態初期化とリセット処理の実践的なパターンを紹介し、アプリケーションの安定性とユーザー体験を向上させる方法を解説いたします。
背景
状態管理における初期化とリセットの必要性
現代の Web アプリケーションでは、複雑な状態管理が求められます。ユーザーの操作履歴、フォームの入力状態、API レスポンスのキャッシュなど、様々な状態がアプリケーション全体で共有されています。
これらの状態は、適切なタイミングで初期化やリセットが行われないと、以下のような問題を引き起こします:
- セキュリティ上の問題(認証情報の漏洩)
- ユーザー体験の低下(古いデータの表示)
- アプリケーションの不整合状態
- メモリリークの発生
一般的な状態管理ライブラリでの課題
Redux や Context API などの他の状態管理ライブラリでは、状態のリセット処理が複雑になりがちです。特に、以下の点で課題があります:
- アクションの定義が冗長になる
- リセット処理の実装が分散しやすい
- 型安全性の確保が困難
- パフォーマンスへの影響が大きい
Zustand での状態リセットの特徴
Zustand は、シンプルな API を提供しながらも、柔軟な状態リセット機能を備えています。その特徴は以下の通りです:
- ストア内での直接的な状態更新
- TypeScript との優れた統合
- 軽量で高速な状態管理
- 直感的な API 設計
課題
状態の永続化による予期しない動作
Zustand で persist middleware を使用している場合、ブラウザのローカルストレージに状態が保存されます。これにより、以下のような問題が発生する可能性があります:
typescript// 問題のある実装例
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UserStore {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
const useUserStore = create<UserStore>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () =>
set({ user: null, isAuthenticated: false }),
}),
{
name: 'user-storage',
}
)
);
この実装では、ログアウト時に状態がリセットされても、ローカルストレージから古い状態が復元されてしまう可能性があります。
複数コンポーネント間での状態共有時のリセット漏れ
複数のコンポーネントが同じストアを参照している場合、一部のコンポーネントでのリセット処理が他のコンポーネントに影響しないことがあります:
typescript// リセット漏れが発生する例
const ComponentA = () => {
const { resetForm } = useFormStore();
const handleSubmit = () => {
// フォーム送信処理
resetForm(); // このリセットがComponentBに反映されない可能性
};
return <button onClick={handleSubmit}>送信</button>;
};
const ComponentB = () => {
const { formData } = useFormStore();
// ComponentAでリセットされても、このコンポーネントの状態が
// 更新されない可能性がある
return <div>{JSON.stringify(formData)}</div>;
};
ユーザーセッション終了時の状態クリーンアップ
ユーザーがログアウトした際、すべての関連する状態を適切にクリーンアップする必要があります:
typescript// 不完全なクリーンアップの例
const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
logout: () => {
set({ user: null, token: null });
// 他のストアの状態がクリーンアップされていない
},
}));
開発・テスト環境での状態リセットの複雑さ
開発やテスト環境では、状態を簡単にリセットできる仕組みが必要です:
typescript// テスト環境での状態リセットが困難な例
describe('UserStore', () => {
beforeEach(() => {
// ストアの状態をリセットする方法がない
});
it('should handle login', () => {
// 前のテストの状態が影響する可能性
});
});
解決策
パターン 1:初期状態オブジェクトを活用したリセット
最もシンプルで効果的なパターンは、初期状態を定数として定義し、リセット時にその状態に戻す方法です。
初期状態を定数として定義
typescript// 初期状態の定義
const initialState = {
user: null,
isAuthenticated: false,
loading: false,
error: null,
} as const;
// 型定義
interface UserState {
user: User | null;
isAuthenticated: boolean;
loading: boolean;
error: string | null;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
reset: () => void;
}
リセット関数での初期状態への復元
typescript// ストアの実装
const useUserStore = create<UserState>((set, get) => ({
...initialState,
login: async (credentials) => {
set({ loading: true, error: null });
try {
const user = await loginAPI(credentials);
set({ user, isAuthenticated: true, loading: false });
} catch (error) {
set({
error:
error instanceof Error
? error.message
: 'ログインに失敗しました',
loading: false,
});
}
},
logout: () => {
set(initialState);
},
reset: () => {
set(initialState);
},
}));
実装例とコードサンプル
このパターンの利点は、初期状態が明確に定義されており、リセット処理が予測可能であることです:
typescript// 使用例
const UserProfile = () => {
const { user, logout, reset } = useUserStore();
const handleLogout = () => {
logout(); // 初期状態にリセット
};
const handleReset = () => {
reset(); // 明示的なリセット
};
return (
<div>
<h1>{user?.name}</h1>
<button onClick={handleLogout}>ログアウト</button>
<button onClick={handleReset}>リセット</button>
</div>
);
};
パターン 2:reset 関数を明示的に定義
より細かい制御が必要な場合は、明示的な reset 関数を定義します。
ストア内での reset 関数の実装
typescriptinterface FormStore {
formData: FormData;
errors: Record<string, string>;
isSubmitting: boolean;
reset: () => void;
resetErrors: () => void;
resetSubmitting: () => void;
}
const useFormStore = create<FormStore>((set) => ({
formData: {},
errors: {},
isSubmitting: false,
reset: () => {
set({
formData: {},
errors: {},
isSubmitting: false,
});
},
resetErrors: () => {
set({ errors: {} });
},
resetSubmitting: () => {
set({ isSubmitting: false });
},
}));
部分的な状態リセットの実現
typescript// 部分的なリセットの実装
interface CartStore {
items: CartItem[];
total: number;
discount: number;
resetAll: () => void;
resetItems: () => void;
resetDiscount: () => void;
}
const useCartStore = create<CartStore>((set) => ({
items: [],
total: 0,
discount: 0,
resetAll: () => {
set({ items: [], total: 0, discount: 0 });
},
resetItems: () => {
set({ items: [], total: 0 });
},
resetDiscount: () => {
set({ discount: 0 });
},
}));
複数ストア間での連携リセット
typescript// 複数ストアの連携リセット
const resetAllStores = () => {
useUserStore.getState().reset();
useFormStore.getState().reset();
useCartStore.getState().resetAll();
};
// または、カスタムフックとして実装
const useGlobalReset = () => {
const resetUser = useUserStore((state) => state.reset);
const resetForm = useFormStore((state) => state.reset);
const resetCart = useCartStore((state) => state.resetAll);
return () => {
resetUser();
resetForm();
resetCart();
};
};
パターン 3:Middleware を活用した自動リセット
Zustand の middleware 機能を活用して、自動的なリセット処理を実装できます。
persist middleware との組み合わせ
typescript// persist middlewareと組み合わせたリセット
const useUserStore = create<UserState>()(
persist(
(set, get) => ({
...initialState,
login: async (credentials) => {
// ログイン処理
set({ user, isAuthenticated: true });
},
logout: () => {
// ローカルストレージも含めて完全リセット
set(initialState);
// persist middlewareのストレージをクリア
localStorage.removeItem('user-storage');
},
}),
{
name: 'user-storage',
// 特定の条件でのみ永続化
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
)
);
カスタム middleware でのリセット処理
typescript// カスタムリセットmiddleware
const resetMiddleware = (config) => (set, get, api) => {
const initialState = config.initialState
return config(
(...args) => {
set(...args)
},
get,
{
...api,
reset: () => set(initialState),
resetTo: (newState) => set(newState)
}
)
}
// 使用例
const useCounterStore = create(
resetMiddleware({
initialState: { count: 0 },
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
})
})
)
条件付きリセットの実装
typescript// 条件付きリセットmiddleware
const conditionalResetMiddleware = (config) => (set, get, api) => {
const { shouldReset, resetCondition } = config
return config(
(...args) => {
set(...args)
// 条件を満たした場合に自動リセット
if (shouldReset && resetCondition(get())) {
set(config.initialState)
}
},
get,
api
)
}
// 使用例
const useSessionStore = create(
conditionalResetMiddleware({
initialState: { sessionId: null, lastActivity: null },
shouldReset: true,
resetCondition: (state) => {
// 30分以上アクティビティがない場合にリセット
return state.lastActivity &&
Date.now() - state.lastActivity > 30 * 60 * 1000
},
(set) => ({
sessionId: null,
lastActivity: null,
updateActivity: () => set({ lastActivity: Date.now() })
})
})
)
パターン 4:React Hooks との連携
React Hooks と組み合わせることで、より柔軟なリセット処理を実現できます。
useEffect でのクリーンアップ処理
typescript// コンポーネントアンマウント時の自動リセット
const UserProfile = () => {
const { user, reset } = useUserStore();
useEffect(() => {
// コンポーネントマウント時の処理
return () => {
// アンマウント時にリセット
reset();
};
}, [reset]);
return <div>{user?.name}</div>;
};
コンポーネントアンマウント時の自動リセット
typescript// カスタムフックでの自動リセット
const useAutoReset = (store, resetFunction) => {
useEffect(() => {
return () => {
resetFunction();
};
}, [resetFunction]);
};
// 使用例
const FormComponent = () => {
const { formData, reset } = useFormStore();
// コンポーネントアンマウント時に自動リセット
useAutoReset(useFormStore, reset);
return <form>{/* フォーム内容 */}</form>;
};
カスタムフックでのリセット機能
typescript// リセット機能付きカスタムフック
const useResetableStore = (store, initialState) => {
const [state, actions] = store();
const reset = useCallback(() => {
actions.reset(initialState);
}, [actions, initialState]);
const resetTo = useCallback(
(newState) => {
actions.resetTo(newState);
},
[actions]
);
return [state, { ...actions, reset, resetTo }];
};
// 使用例
const useUserStoreWithReset = () => {
return useResetableStore(useUserStore, initialState);
};
パターン 5:TypeScript 型システムを活用
TypeScript の型システムを活用することで、型安全なリセット処理を実装できます。
型安全なリセット処理
typescript// 型安全なリセット関数の定義
type ResetFunction<T> = () => T;
type PartialResetFunction<T, K extends keyof T> = (
keys: K[]
) => Pick<T, K>;
interface TypedStore<T> {
state: T;
reset: ResetFunction<T>;
resetPartial: PartialResetFunction<T, keyof T>;
}
// 型安全なストアの実装
const createTypedStore = <T extends Record<string, any>>(
initialState: T
): TypedStore<T> => {
const store = create<
T & {
reset: () => void;
resetPartial: (keys: (keyof T)[]) => void;
}
>((set, get) => ({
...initialState,
reset: () => {
set(initialState);
},
resetPartial: (keys) => {
const currentState = get();
const newState = { ...currentState };
keys.forEach((key) => {
newState[key] = initialState[key];
});
set(newState);
},
}));
return {
state: store.getState(),
reset: store.getState().reset,
resetPartial: store.getState().resetPartial,
};
};
部分的な状態リセットの型定義
typescript// 部分的なリセットの型定義
type ResetableKeys<T> = {
[K in keyof T]: T[K] extends object ? never : K;
}[keyof T];
interface PartialResetStore<T> {
state: T;
reset: () => void;
resetField: <K extends ResetableKeys<T>>(
field: K
) => void;
resetFields: <K extends ResetableKeys<T>>(
fields: K[]
) => void;
}
// 実装例
const useTypedFormStore = create<
FormState & PartialResetStore<FormState>
>((set, get) => ({
name: '',
email: '',
age: 0,
reset: () => {
set({ name: '', email: '', age: 0 });
},
resetField: (field) => {
const initialState = { name: '', email: '', age: 0 };
set({
[field]: initialState[field],
} as Partial<FormState>);
},
resetFields: (fields) => {
const initialState = { name: '', email: '', age: 0 };
const resetData = fields.reduce((acc, field) => {
acc[field] = initialState[field];
return acc;
}, {} as Partial<FormState>);
set(resetData);
},
}));
リセット関数の型制約
typescript// リセット関数の型制約
type ResetConstraint<T> = {
[K in keyof T]: T[K] extends Function ? never : T[K];
};
interface ConstrainedStore<T> {
state: ResetConstraint<T>;
reset: () => ResetConstraint<T>;
resetWithValidation: (
newState: Partial<ResetConstraint<T>>
) => void;
}
// バリデーション付きリセット
const createValidatedStore = <
T extends Record<string, any>
>(
initialState: T,
validator: (state: T) => boolean
) => {
return create<T & ConstrainedStore<T>>((set, get) => ({
...initialState,
reset: () => {
if (validator(initialState)) {
set(initialState);
} else {
throw new Error('Invalid initial state');
}
},
resetWithValidation: (newState) => {
const validatedState = { ...get(), ...newState };
if (validator(validatedState)) {
set(validatedState);
} else {
throw new Error('Invalid state for reset');
}
},
}));
};
具体例
ユーザー認証ストアのリセット実装
実際のユーザー認証システムでのリセット処理を実装してみましょう:
typescript// ユーザー認証ストアの完全な実装
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
loading: boolean;
error: string | null;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
reset: () => void;
clearError: () => void;
}
const initialState: Omit<
AuthState,
'login' | 'logout' | 'reset' | 'clearError'
> = {
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: null,
};
const useAuthStore = create<AuthState>((set, get) => ({
...initialState,
login: async (credentials) => {
set({ loading: true, error: null });
try {
const response = await loginAPI(credentials);
set({
user: response.user,
token: response.token,
isAuthenticated: true,
loading: false,
});
} catch (error) {
set({
error:
error instanceof Error
? error.message
: 'ログインに失敗しました',
loading: false,
});
}
},
logout: () => {
// トークンの無効化処理
const { token } = get();
if (token) {
invalidateTokenAPI(token);
}
// 状態の完全リセット
set(initialState);
// ローカルストレージのクリア
localStorage.removeItem('auth-storage');
sessionStorage.clear();
},
reset: () => {
set(initialState);
},
clearError: () => {
set({ error: null });
},
}));
フォーム状態のリセット処理
複雑なフォーム状態のリセット処理を実装します:
typescript// フォーム状態の管理
interface FormState {
data: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isSubmitting: boolean;
isDirty: boolean;
setField: (field: string, value: any) => void;
setError: (field: string, error: string) => void;
setTouched: (field: string, touched: boolean) => void;
reset: () => void;
resetErrors: () => void;
resetTouched: () => void;
resetField: (field: string) => void;
}
const useFormStore = create<FormState>((set, get) => ({
data: {},
errors: {},
touched: {},
isSubmitting: false,
isDirty: false,
setField: (field, value) => {
set((state) => ({
data: { ...state.data, [field]: value },
isDirty: true,
}));
},
setError: (field, error) => {
set((state) => ({
errors: { ...state.errors, [field]: error },
}));
},
setTouched: (field, touched) => {
set((state) => ({
touched: { ...state.touched, [field]: touched },
}));
},
reset: () => {
set({
data: {},
errors: {},
touched: {},
isSubmitting: false,
isDirty: false,
});
},
resetErrors: () => {
set({ errors: {} });
},
resetTouched: () => {
set({ touched: {} });
},
resetField: (field) => {
set((state) => {
const newData = { ...state.data };
const newErrors = { ...state.errors };
const newTouched = { ...state.touched };
delete newData[field];
delete newErrors[field];
delete newTouched[field];
return {
data: newData,
errors: newErrors,
touched: newTouched,
};
});
},
}));
ページ遷移時の状態クリーンアップ
Next.js でのページ遷移時の状態クリーンアップを実装します:
typescript// ページ遷移時の状態管理
interface PageState {
currentPage: string;
pageData: Record<string, any>;
isLoading: boolean;
setPageData: (page: string, data: any) => void;
clearPageData: (page?: string) => void;
resetAllPages: () => void;
}
const usePageStore = create<PageState>((set, get) => ({
currentPage: '',
pageData: {},
isLoading: false,
setPageData: (page, data) => {
set((state) => ({
pageData: { ...state.pageData, [page]: data },
}));
},
clearPageData: (page) => {
if (page) {
set((state) => {
const newPageData = { ...state.pageData };
delete newPageData[page];
return { pageData: newPageData };
});
} else {
set({ pageData: {} });
}
},
resetAllPages: () => {
set({
currentPage: '',
pageData: {},
isLoading: false,
});
},
}));
// Next.jsでの使用例
const usePageTransition = () => {
const { clearPageData, resetAllPages } = usePageStore();
useEffect(() => {
const handleRouteChange = (url: string) => {
// 特定のページ遷移時にデータをクリア
if (url.includes('/admin')) {
clearPageData('user-dashboard');
}
};
const handleRouteChangeComplete = () => {
// ページ遷移完了時の処理
};
router.events.on('routeChangeStart', handleRouteChange);
router.events.on(
'routeChangeComplete',
handleRouteChangeComplete
);
return () => {
router.events.off(
'routeChangeStart',
handleRouteChange
);
router.events.off(
'routeChangeComplete',
handleRouteChangeComplete
);
};
}, [clearPageData]);
return { resetAllPages };
};
エラー状態のリセットパターン
エラー状態の適切なリセット処理を実装します:
typescript// エラー状態の管理
interface ErrorState {
errors: Record<string, ErrorInfo>;
globalError: string | null;
addError: (key: string, error: ErrorInfo) => void;
removeError: (key: string) => void;
clearErrors: () => void;
setGlobalError: (error: string | null) => void;
resetAllErrors: () => void;
}
interface ErrorInfo {
message: string;
code: string;
timestamp: number;
retryable: boolean;
}
const useErrorStore = create<ErrorState>((set, get) => ({
errors: {},
globalError: null,
addError: (key, error) => {
set((state) => ({
errors: {
...state.errors,
[key]: { ...error, timestamp: Date.now() },
},
}));
},
removeError: (key) => {
set((state) => {
const newErrors = { ...state.errors };
delete newErrors[key];
return { errors: newErrors };
});
},
clearErrors: () => {
set({ errors: {} });
},
setGlobalError: (error) => {
set({ globalError: error });
},
resetAllErrors: () => {
set({ errors: {}, globalError: null });
},
}));
// エラーハンドリングコンポーネント
const ErrorBoundary = ({
children,
}: {
children: React.ReactNode;
}) => {
const { globalError, resetAllErrors } = useErrorStore();
useEffect(() => {
// 一定時間後にエラーを自動クリア
if (globalError) {
const timer = setTimeout(() => {
resetAllErrors();
}, 5000);
return () => clearTimeout(timer);
}
}, [globalError, resetAllErrors]);
if (globalError) {
return (
<div className='error-boundary'>
<p>{globalError}</p>
<button onClick={resetAllErrors}>
エラーをクリア
</button>
</div>
);
}
return <>{children}</>;
};
まとめ
各パターンの使い分けと選択基準
今回紹介した 5 つのパターンは、それぞれ異なるユースケースに適しています:
-
初期状態オブジェクトを活用したリセット
- シンプルな状態管理に最適
- 予測可能なリセット処理が必要な場合
- 小規模から中規模のアプリケーション
-
reset 関数を明示的に定義
- 部分的なリセットが必要な場合
- 複雑な状態構造を持つアプリケーション
- 細かい制御が必要な場合
-
Middleware を活用した自動リセット
- 永続化が必要な状態管理
- 条件付きの自動リセットが必要な場合
- 高度な状態管理機能が必要な場合
-
React Hooks との連携
- コンポーネントライフサイクルと連携が必要な場合
- 自動的なクリーンアップが必要な場合
- カスタムフックでの再利用性を重視する場合
-
TypeScript 型システムを活用
- 型安全性を重視する場合
- 大規模なアプリケーション開発
- チーム開発での保守性を重視する場合
実装時の注意点とベストプラクティス
-
初期状態の一貫性
- 初期状態は必ず定数として定義する
- 型定義と初期状態の整合性を保つ
- 初期状態の変更時は影響範囲を確認する
-
リセット処理のタイミング
- ユーザーアクションに応じた適切なタイミングでリセット
- コンポーネントのライフサイクルを考慮
- パフォーマンスへの影響を最小限に抑える
-
エラーハンドリング
- リセット処理中のエラーを適切に処理
- 部分的なリセット失敗時のフォールバック
- ユーザーへの適切なフィードバック
-
テストの実装
- リセット処理の単体テスト
- 統合テストでの状態リセットの確認
- エラーケースのテスト
パフォーマンスと保守性のバランス
-
メモリ使用量の最適化
- 不要な状態の保持を避ける
- 適切なタイミングでのガベージコレクション
- 大きなオブジェクトの参照を避ける
-
再レンダリングの最適化
- 必要な部分のみをリセット
- 不要な再レンダリングを防ぐ
- selector の適切な使用
-
コードの保守性
- リセット処理の一貫性を保つ
- ドキュメントの整備
- 命名規則の統一
Zustand での状態初期化とリセット処理は、アプリケーションの安定性とユーザー体験に直接影響します。適切なパターンを選択し、実装することで、予測可能で保守しやすい状態管理システムを構築できます。
特に、ユーザーのセキュリティとプライバシーを保護するため、認証情報や個人情報を含む状態の適切なリセットは重要です。また、開発効率を向上させるため、テスト環境での状態リセット機能も忘れずに実装しましょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来