Vite でグローバル状態管理を実現する方法

Vite は高速なビルドツールとして人気を集めていますが、実際のアプリケーション開発では状態管理が重要な課題となります。コンポーネント間でのデータ共有や、アプリケーション全体の状態を効率的に管理する方法を知ることで、より保守性の高いアプリケーションを構築できるようになります。
この記事では、Vite プロジェクトでグローバル状態管理を実現する様々な方法を、実際のコード例とエラーケースを含めて詳しく解説していきます。初心者の方でも理解しやすいように、段階的に説明していきましょう。
グローバル状態管理の必要性
なぜグローバル状態管理が必要なのか
モダンな Web アプリケーションでは、複数のコンポーネントが同じデータを共有する必要があります。例えば、ユーザー情報、認証状態、ショッピングカートの内容などです。
javascript// 問題のある例:props drilling
function App() {
const [user, setUser] = useState(null);
return (
<div>
<Header user={user} />
<MainContent user={user} />
<Sidebar user={user} />
<Footer user={user} />
</div>
);
}
この方法では、データを必要としない中間コンポーネントにも props を渡す必要があり、コードが複雑になってしまいます。
グローバル状態管理のメリット
- データの一元管理: アプリケーション全体の状態を一箇所で管理
- コンポーネント間の疎結合: props drilling を避けられる
- デバッグの容易さ: 状態の変化を追跡しやすい
- パフォーマンスの最適化: 必要な部分のみ再レンダリング
Vite プロジェクトでの状態管理の選択肢
Vite はフレームワークに依存しないため、様々な状態管理ライブラリを使用できます。主な選択肢を比較してみましょう。
ライブラリ | 学習コスト | バンドルサイズ | 型安全性 | 適用場面 |
---|---|---|---|---|
Context API | 低 | 小 | 中 | 小〜中規模 |
Zustand | 低 | 小 | 高 | 小〜大規模 |
Redux Toolkit | 中 | 中 | 高 | 大規模 |
Pinia | 低 | 小 | 高 | Vue.js |
Context API を使った状態管理
Context API の基本概念
React の Context API は、追加のライブラリなしでグローバル状態管理を実現できる組み込み機能です。
まず、Vite プロジェクトで Context API を使用する基本的なセットアップを見てみましょう。
typescript// src/contexts/UserContext.tsx
import React, {
createContext,
useContext,
useState,
ReactNode,
} from 'react';
// ユーザー情報の型定義
interface User {
id: string;
name: string;
email: string;
}
// Contextの型定義
interface UserContextType {
user: User | null;
login: (userData: User) => void;
logout: () => void;
}
// Contextの作成
const UserContext = createContext<
UserContextType | undefined
>(undefined);
// Providerコンポーネント
export function UserProvider({
children,
}: {
children: ReactNode;
}) {
const [user, setUser] = useState<User | null>(null);
const login = (userData: User) => {
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// カスタムフック
export function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error(
'useUser must be used within a UserProvider'
);
}
return context;
}
Context API の実装例
次に、実際に Context API を使用するコンポーネントの例を見てみましょう。
typescript// src/components/LoginForm.tsx
import React, { useState } from 'react';
import { useUser } from '../contexts/UserContext';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useUser();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 実際のアプリケーションではAPI呼び出しを行う
const userData = {
id: '1',
name: 'テストユーザー',
email: email,
};
login(userData);
};
return (
<form onSubmit={handleSubmit}>
<input
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder='メールアドレス'
required
/>
<input
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='パスワード'
required
/>
<button type='submit'>ログイン</button>
</form>
);
}
Context API のよくあるエラーと解決策
Context API を使用する際によく遭遇するエラーを紹介します。
typescript// エラー例1: ContextがProviderで囲まれていない
function MyComponent() {
const { user } = useUser(); // Error: useUser must be used within a UserProvider
return <div>{user?.name}</div>;
}
// 解決策: App.tsxでProviderで囲む
function App() {
return (
<UserProvider>
<MyComponent />
</UserProvider>
);
}
typescript// エラー例2: 型定義の不備
const UserContext = createContext(null); // 型安全性が失われる
// 解決策: 適切な型定義
const UserContext = createContext<
UserContextType | undefined
>(undefined);
Zustand を使った状態管理
Zustand の特徴と利点
Zustand は軽量で直感的な状態管理ライブラリです。Redux の複雑さを避けながら、強力な機能を提供します。
まず、Zustand を Vite プロジェクトにインストールしましょう。
bashyarn add zustand
Zustand ストアの作成
Zustand での基本的なストア作成方法を見てみましょう。
typescript// src/stores/userStore.ts
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
}
interface UserStore {
user: User | null;
isLoading: boolean;
error: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
clearError: () => void;
}
export const useUserStore = create<UserStore>(
(set, get) => ({
user: null,
isLoading: false,
error: null,
login: async (email: string, password: string) => {
set({ isLoading: true, error: null });
try {
// 実際のAPI呼び出しをシミュレート
await new Promise((resolve) =>
setTimeout(resolve, 1000)
);
const userData = {
id: '1',
name: 'テストユーザー',
email: email,
};
set({ user: userData, isLoading: false });
} catch (error) {
set({
error: 'ログインに失敗しました',
isLoading: false,
});
}
},
logout: () => {
set({ user: null, error: null });
},
clearError: () => {
set({ error: null });
},
})
);
Zustand を使用したコンポーネント
Zustand ストアを使用するコンポーネントの実装例です。
typescript// src/components/UserProfile.tsx
import React from 'react';
import { useUserStore } from '../stores/userStore';
export function UserProfile() {
const { user, logout } = useUserStore();
if (!user) {
return <div>ログインしてください</div>;
}
return (
<div>
<h2>ユーザープロフィール</h2>
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
<button onClick={logout}>ログアウト</button>
</div>
);
}
Zustand の高度な機能
Zustand には、状態の永続化やデバッグ機能など、便利な機能があります。
typescript// src/stores/persistentStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Settings {
theme: 'light' | 'dark';
language: 'ja' | 'en';
}
interface SettingsStore {
settings: Settings;
updateTheme: (theme: Settings['theme']) => void;
updateLanguage: (language: Settings['language']) => void;
}
export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
settings: {
theme: 'light',
language: 'ja',
},
updateTheme: (theme) =>
set((state) => ({
settings: { ...state.settings, theme },
})),
updateLanguage: (language) =>
set((state) => ({
settings: { ...state.settings, language },
})),
}),
{
name: 'settings-storage', // localStorageのキー名
}
)
);
Redux Toolkit を使った状態管理
Redux Toolkit の導入
大規模なアプリケーションでは、Redux Toolkit が強力な選択肢となります。
bashyarn add @reduxjs/toolkit react-redux
Redux Toolkit ストアの設定
Redux Toolkit での基本的なストア設定を見てみましょう。
typescript// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import cartReducer from './cartSlice';
export const store = configureStore({
reducer: {
user: userReducer,
cart: cartReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Redux Toolkit スライスの作成
Redux Toolkit のスライス機能を使用した状態管理の実装例です。
typescript// src/store/userSlice.ts
import {
createSlice,
createAsyncThunk,
PayloadAction,
} from '@reduxjs/toolkit';
interface User {
id: string;
name: string;
email: string;
}
interface UserState {
user: User | null;
isLoading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
isLoading: false,
error: null,
};
// 非同期アクション
export const loginUser = createAsyncThunk(
'user/login',
async (credentials: {
email: string;
password: string;
}) => {
// 実際のAPI呼び出しをシミュレート
await new Promise((resolve) =>
setTimeout(resolve, 1000)
);
return {
id: '1',
name: 'テストユーザー',
email: credentials.email,
};
}
);
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
state.user = null;
state.error = null;
},
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload;
})
.addCase(loginUser.rejected, (state, action) => {
state.isLoading = false;
state.error =
action.error.message || 'ログインに失敗しました';
});
},
});
export const { logout, clearError } = userSlice.actions;
export default userSlice.reducer;
Redux Toolkit の使用例
Redux Toolkit を使用するコンポーネントの実装例です。
typescript// src/components/ReduxLoginForm.tsx
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { loginUser } from '../store/userSlice';
import { RootState, AppDispatch } from '../store';
export function ReduxLoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useDispatch<AppDispatch>();
const { isLoading, error } = useSelector(
(state: RootState) => state.user
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
dispatch(loginUser({ email, password }));
};
return (
<form onSubmit={handleSubmit}>
{error && <div style={{ color: 'red' }}>{error}</div>}
<input
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder='メールアドレス'
required
/>
<input
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='パスワード'
required
/>
<button type='submit' disabled={isLoading}>
{isLoading ? 'ログイン中...' : 'ログイン'}
</button>
</form>
);
}
Pinia を使った状態管理(Vue.js)
Pinia の特徴
Vue.js を使用している場合、Pinia が推奨される状態管理ライブラリです。
bashyarn add pinia
Pinia ストアの作成
Pinia での基本的なストア作成方法を見てみましょう。
typescript// src/stores/user.ts
import { defineStore } from 'pinia';
interface User {
id: string;
name: string;
email: string;
}
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
isLoading: false,
error: null as string | null,
}),
getters: {
isLoggedIn: (state) => !!state.user,
userName: (state) => state.user?.name || 'ゲスト',
},
actions: {
async login(email: string, password: string) {
this.isLoading = true;
this.error = null;
try {
// 実際のAPI呼び出しをシミュレート
await new Promise((resolve) =>
setTimeout(resolve, 1000)
);
this.user = {
id: '1',
name: 'テストユーザー',
email: email,
};
} catch (error) {
this.error = 'ログインに失敗しました';
} finally {
this.isLoading = false;
}
},
logout() {
this.user = null;
this.error = null;
},
clearError() {
this.error = null;
},
},
});
Pinia の使用例
Pinia ストアを使用する Vue コンポーネントの実装例です。
vue<!-- src/components/UserProfile.vue -->
<template>
<div>
<div v-if="userStore.isLoading">読み込み中...</div>
<div v-else-if="userStore.error" class="error">
{{ userStore.error }}
</div>
<div v-else-if="userStore.isLoggedIn">
<h2>ユーザープロフィール</h2>
<p>名前: {{ userStore.userName }}</p>
<p>メール: {{ userStore.user?.email }}</p>
<button @click="userStore.logout">ログアウト</button>
</div>
<div v-else>
<p>ログインしてください</p>
</div>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '../stores/user';
const userStore = useUserStore();
</script>
<style scoped>
.error {
color: red;
margin-bottom: 1rem;
}
</style>
パフォーマンスの比較
各ライブラリのパフォーマンス特性
実際のプロジェクトで使用する際のパフォーマンスを比較してみましょう。
ライブラリ | 初期バンドルサイズ | 実行時パフォーマンス | メモリ使用量 | 学習コスト |
---|---|---|---|---|
Context API | 0KB | 中 | 低 | 低 |
Zustand | ~2KB | 高 | 低 | 低 |
Redux Toolkit | ~15KB | 高 | 中 | 中 |
Pinia | ~5KB | 高 | 低 | 低 |
パフォーマンス最適化のベストプラクティス
typescript// 最適化例:Zustandでの選択的サブスクライブ
import { useUserStore } from '../stores/userStore';
// 悪い例:全体のストアをサブスクライブ
function BadComponent() {
const store = useUserStore(); // 全体が再レンダリングされる
return <div>{store.user?.name}</div>;
}
// 良い例:必要な部分のみサブスクライブ
function GoodComponent() {
const userName = useUserStore(
(state) => state.user?.name
);
return <div>{userName}</div>;
}
実装例とベストプラクティス
プロジェクト構造の推奨パターン
効率的な状態管理のためのプロジェクト構造を提案します。
bashsrc/
├── stores/ # 状態管理ストア
│ ├── index.ts # ストアのエクスポート
│ ├── userStore.ts
│ └── cartStore.ts
├── hooks/ # カスタムフック
│ ├── useAuth.ts
│ └── useCart.ts
├── types/ # 型定義
│ └── store.ts
└── utils/ # ユーティリティ
└── storeHelpers.ts
エラーハンドリングの実装
実用的なエラーハンドリングの実装例です。
typescript// src/stores/errorStore.ts
import { create } from 'zustand';
interface ErrorState {
errors: Map<string, string>;
addError: (key: string, message: string) => void;
removeError: (key: string) => void;
clearErrors: () => void;
}
export const useErrorStore = create<ErrorState>((set) => ({
errors: new Map(),
addError: (key, message) =>
set((state) => {
const newErrors = new Map(state.errors);
newErrors.set(key, message);
return { errors: newErrors };
}),
removeError: (key) =>
set((state) => {
const newErrors = new Map(state.errors);
newErrors.delete(key);
return { errors: newErrors };
}),
clearErrors: () => set({ errors: new Map() }),
}));
状態の永続化
重要な状態をローカルストレージに保存する実装例です。
typescript// src/utils/persistence.ts
import { useUserStore } from '../stores/userStore';
// 状態の永続化
export const persistUserState = () => {
const user = useUserStore.getState().user;
if (user) {
localStorage.setItem('user', JSON.stringify(user));
}
};
// 状態の復元
export const restoreUserState = () => {
const savedUser = localStorage.getItem('user');
if (savedUser) {
try {
const user = JSON.parse(savedUser);
useUserStore.getState().login(user.email, '');
} catch (error) {
console.error('Failed to restore user state:', error);
}
}
};
よくあるエラーとトラブルシューティング
実際の開発で遭遇しやすいエラーとその解決策を紹介します。
typescript// エラー例1: 無限ループ
function BadComponent() {
const [count, setCount] = useState(0);
// 悪い例:レンダリング時に毎回新しいオブジェクトを作成
const userStore = useUserStore();
useEffect(() => {
setCount(count + 1); // 無限ループが発生
}, [userStore]); // userStoreが毎回新しいオブジェクトになる
return <div>{count}</div>;
}
// 解決策:必要な値のみをサブスクライブ
function GoodComponent() {
const [count, setCount] = useState(0);
const userName = useUserStore(
(state) => state.user?.name
);
useEffect(() => {
setCount(count + 1);
}, [userName]); // 特定の値の変更のみを監視
return <div>{count}</div>;
}
typescript// エラー例2: 型安全性の問題
// 悪い例:型定義なし
const store = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
// 解決策:適切な型定義
interface UserStore {
user: User | null;
setUser: (user: User | null) => void;
}
const store = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
まとめ
Vite プロジェクトでのグローバル状態管理について、様々なアプローチを詳しく解説してきました。
重要なポイントを振り返ると:
- Context APIは小〜中規模のアプリケーションに適しており、追加のライブラリが不要
- Zustandは軽量で直感的、学習コストが低く、多くのプロジェクトに適している
- Redux Toolkitは大規模アプリケーションで強力な機能を提供するが、学習コストが高い
- Piniaは Vue.js プロジェクトで推奨される状態管理ライブラリ
選択の基準として:
- プロジェクトの規模
- チームの技術レベル
- パフォーマンス要件
- メンテナンス性
を総合的に考慮して、最適な状態管理ライブラリを選択することが重要です。
状態管理は、アプリケーションの品質と保守性に直結する重要な要素です。今回紹介した方法を参考に、プロジェクトに最適な状態管理を実装していただければと思います。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来