React useContext入門|グローバルな状態管理を簡単に
React useContextの基本的な使い方から実践的な活用方法まで詳しく解説。グローバルな状態管理を簡単に実装する方法を学べます。
みなさん、Reactの状態管理で困ったことはありませんか?
「コンポーネントが深くなるとpropsの受け渡しが大変」 「ユーザー情報を色々な場所で使いたいけど、どうすればいいの?」 「ReduxやZustandは難しそう...」
こんなふうに思ったことはありませんか?
そんな時に役立つのが、React useContextです。 この記事では、useContextを使った簡単なグローバル状態管理について、基本から実践的な使い方まで詳しく解説します。
コードが複雑になりがちな状態管理を、スッキリと整理する方法を一緒に学んでいきましょう。
useContextって何?
useContextは、Reactでグローバルな状態を管理するためのフックです。
簡単に言うと、データの倉庫を作って、どのコンポーネントからでもアクセスできる仕組みなんです。
propsの受け渡し問題
まず、useContextがなぜ便利なのか確認してみましょう。
// ❌ propsのバケツリレー(深いコンポーネント階層)
function App() {
const [user, setUser] = useState({ name: '田中太郎' });
return <Dashboard user={user} setUser={setUser} />;
}
function Dashboard({ user, setUser }) {
return <Sidebar user={user} setUser={setUser} />;
}
function Sidebar({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}
function UserMenu({ user, setUser }) {
return <UserProfile user={user} setUser={setUser} />;
}
function UserProfile({ user, setUser }) {
return <div>{user.name}</div>;
}
このように、propsを何層にも渡していくのは大変ですよね。 useContextを使うと、この問題をスッキリ解決できます。
useContextの3つの要素
useContextは3つの要素で成り立っています。
- Context: データを保管する「箱」
- Provider: データを提供する「配達員」
- useContext: データを受け取る「受信機」
イメージとしては、データ放送局みたいな感じです。 Provider(放送局)がデータを送信して、useContext(ラジオ)でそれを受信するんです。
useContextの利点
useContextを使うメリットはたくさんあります。
- propsの受け渡しが不要になる
- コードがスッキリして読みやすくなる
- データの管理が一箇所に集約される
- 再利用性が高いコンポーネントが作れる
でも大丈夫です! 使い方は思っているより簡単ですよ。
基本的な使い方
useContextの基本的な使い方を見てみましょう。
シンプルなユーザー管理
まずは、ユーザー情報を管理する簡単な例から始めます。
import React, { createContext, useContext, useState } from 'react';
// 1. Context の作成
const UserContext = createContext();
// 2. Provider コンポーネントの作成
function UserProvider({ children }) {
const [user, setUser] = useState({
name: '田中太郎',
email: 'tanaka@example.com',
role: 'admin'
});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// 3. useContext を使用するコンポーネント
function UserProfile() {
const { user } = useContext(UserContext);
return (
<div>
<h2>ユーザープロフィール</h2>
<p>名前: {user.name}</p>
<p>メールアドレス: {user.email}</p>
<p>役割: {user.role}</p>
</div>
);
}
function UserSettings() {
const { user, setUser } = useContext(UserContext);
const handleNameChange = (newName) => {
setUser(prev => ({ ...prev, name: newName }));
};
return (
<div>
<h2>ユーザー設定</h2>
<input
type="text"
value={user.name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="名前を入力"
/>
</div>
);
}
// 4. アプリケーションで使用
function App() {
return (
<UserProvider>
<div>
<h1>アプリケーション</h1>
<UserProfile />
<UserSettings />
</div>
</UserProvider>
);
}
この例では、UserProvider
の中にあるコンポーネントなら、どこからでもuser
にアクセスできます。
propsを渡す必要がないので、とてもスッキリしていますね。
カスタムフックで使いやすく
もう少し使いやすくするために、カスタムフックを作ってみましょう。
import React, { createContext, useContext, useState } from 'react';
const UserContext = createContext();
// カスタムフック
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
function UserProvider({ children }) {
const [user, setUser] = useState({
name: '田中太郎',
email: 'tanaka@example.com',
role: 'admin'
});
const updateUser = (updates) => {
setUser(prev => ({ ...prev, ...updates }));
};
const resetUser = () => {
setUser({
name: '',
email: '',
role: 'user'
});
};
return (
<UserContext.Provider value={{
user,
setUser,
updateUser,
resetUser
}}>
{children}
</UserContext.Provider>
);
}
// 使用例
function UserProfile() {
const { user } = useUser(); // カスタムフックを使用
return (
<div>
<h2>ユーザープロフィール</h2>
<p>名前: {user.name}</p>
<p>メールアドレス: {user.email}</p>
<p>役割: {user.role}</p>
</div>
);
}
function UserSettings() {
const { user, updateUser, resetUser } = useUser();
return (
<div>
<h2>ユーザー設定</h2>
<input
type="text"
value={user.name}
onChange={(e) => updateUser({ name: e.target.value })}
placeholder="名前を入力"
/>
<button onClick={resetUser}>リセット</button>
</div>
);
}
カスタムフックuseUser
を作ることで、以下のメリットがあります。
- エラーハンドリングが自動でできる
- 使いやすいAPIを提供できる
- コードの重複を避けられる
とても便利ですね!
実践的な活用例
実際のアプリでよく使われるパターンを見てみましょう。
テーマ管理システム
ダークモードとライトモードの切り替えを管理してみます。
import React, { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// ローカルストレージから設定を読み込み
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
}
}, []);
// テーマ変更時にローカルストレージに保存
useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const themes = {
light: {
backgroundColor: '#ffffff',
color: '#333333',
primaryColor: '#007bff',
secondaryColor: '#6c757d'
},
dark: {
backgroundColor: '#1a1a1a',
color: '#ffffff',
primaryColor: '#0d6efd',
secondaryColor: '#6c757d'
}
};
return (
<ThemeContext.Provider value={{
theme,
setTheme,
toggleTheme,
colors: themes[theme]
}}>
{children}
</ThemeContext.Provider>
);
}
// テーマ切り替えボタン
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
{theme === 'light' ? 'ダークモード' : 'ライトモード'}
</button>
);
}
// テーマを適用したコンポーネント
function ThemedCard({ title, children }) {
const { colors } = useTheme();
return (
<div style={{
backgroundColor: colors.backgroundColor,
color: colors.color,
border: `1px solid ${colors.secondaryColor}`,
padding: '20px',
borderRadius: '8px',
margin: '10px 0'
}}>
<h3 style={{ color: colors.primaryColor }}>{title}</h3>
{children}
</div>
);
}
この例では、テーマの情報をローカルストレージに保存しています。 ページを更新しても、設定が残るので便利ですね。
認証システム
ユーザーのログイン状態を管理する実用的な例も見てみましょう。
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// アプリケーション起動時に認証状態を確認
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem('authToken');
if (token) {
// トークンを使ってユーザー情報を取得
const response = await fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
localStorage.removeItem('authToken');
}
}
} catch (error) {
console.error('認証確認エラー:', error);
localStorage.removeItem('authToken');
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const login = async (email, password) => {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('authToken', data.token);
setUser(data.user);
return { success: true };
} else {
const error = await response.json();
return { success: false, error: error.message };
}
} catch (error) {
return { success: false, error: 'ログインに失敗しました' };
}
};
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
};
return (
<AuthContext.Provider value={{
user,
loading,
login,
logout,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
);
}
// 認証が必要なコンポーネントを保護するラッパー
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
if (loading) {
return <div>読み込み中...</div>;
}
if (!user) {
return <LoginForm />;
}
return children;
}
// ログインフォーム
function LoginForm() {
const { login } = useAuth();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const result = await login(formData.email, formData.password);
if (!result.success) {
setError(result.error);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>ログイン</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="メールアドレス"
required
/>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="パスワード"
required
/>
<button type="submit">ログイン</button>
</form>
);
}
// ダッシュボード
function Dashboard() {
const { user, logout } = useAuth();
return (
<div>
<h1>ダッシュボード</h1>
<p>ようこそ、{user.name}さん</p>
<button onClick={logout}>ログアウト</button>
</div>
);
}
この認証システムでは、ログイン状態をアプリ全体で管理できます。 ページを更新しても、ログイン状態が保持されるのも便利ですね。
複数のContextを組み合わせる
実際のアプリでは、複数のContextを同時に使うことがよくあります。
Contextの階層化
複数のProviderを効率よく管理する方法を見てみましょう。
import React, { createContext, useContext } from 'react';
// 複数のProvider をまとめるコンポーネント
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
<UserPreferencesProvider>
{children}
</UserPreferencesProvider>
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
}
// 通知システム
const NotificationContext = createContext();
function useNotification() {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification must be used within a NotificationProvider');
}
return context;
}
function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = (message, type = 'info') => {
const id = Date.now();
const notification = { id, message, type };
setNotifications(prev => [...prev, notification]);
// 5秒後に自動削除
setTimeout(() => {
removeNotification(id);
}, 5000);
};
const removeNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<NotificationContext.Provider value={{
notifications,
addNotification,
removeNotification
}}>
{children}
</NotificationContext.Provider>
);
}
// ユーザー設定
const UserPreferencesContext = createContext();
function useUserPreferences() {
const context = useContext(UserPreferencesContext);
if (!context) {
throw new Error('useUserPreferences must be used within a UserPreferencesProvider');
}
return context;
}
function UserPreferencesProvider({ children }) {
const [preferences, setPreferences] = useState({
language: 'ja',
timezone: 'Asia/Tokyo',
notifications: {
email: true,
push: false,
sms: false
}
});
const updatePreferences = (updates) => {
setPreferences(prev => ({ ...prev, ...updates }));
};
return (
<UserPreferencesContext.Provider value={{
preferences,
updatePreferences
}}>
{children}
</UserPreferencesContext.Provider>
);
}
// 複数のContext を使用するコンポーネント
function UserDashboard() {
const { user } = useAuth();
const { theme } = useTheme();
const { addNotification } = useNotification();
const { preferences } = useUserPreferences();
const handleSaveSettings = () => {
// 設定保存処理
addNotification('設定が保存されました', 'success');
};
return (
<div>
<h1>ユーザーダッシュボード</h1>
<p>ユーザー: {user.name}</p>
<p>テーマ: {theme}</p>
<p>言語: {preferences.language}</p>
<button onClick={handleSaveSettings}>設定を保存</button>
</div>
);
}
// 通知表示コンポーネント
function NotificationList() {
const { notifications, removeNotification } = useNotification();
return (
<div className="notification-container">
{notifications.map(notification => (
<div
key={notification.id}
className={`notification ${notification.type}`}
>
<span>{notification.message}</span>
<button onClick={() => removeNotification(notification.id)}>
×
</button>
</div>
))}
</div>
);
}
AppProviders
で複数のProviderをまとめることで、管理がとても楽になります。
各Contextは独立しているので、機能ごとに分けて開発できるのも便利ですね。
パフォーマンスを最適化する
useContextを使う時に気をつけたいパフォーマンスの話をしましょう。
不要な再レンダリングを防ぐ
Contextの値が変わると、それを使うすべてのコンポーネントが再レンダリングされます。 これを最適化する方法を見てみましょう。
import React, { createContext, useContext, useState, useMemo, memo } from 'react';
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState({
name: '田中太郎',
email: 'tanaka@example.com',
settings: {
theme: 'light',
notifications: true
}
});
// Context の値をメモ化
const contextValue = useMemo(() => ({
user,
setUser,
updateUser: (updates) => {
setUser(prev => ({ ...prev, ...updates }));
},
updateSettings: (settingUpdates) => {
setUser(prev => ({
...prev,
settings: { ...prev.settings, ...settingUpdates }
}));
}
}), [user]);
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
}
// メモ化されたコンポーネント
const UserProfile = memo(function UserProfile() {
const { user } = useContext(UserContext);
console.log('UserProfile rendered'); // デバッグ用
return (
<div>
<h2>ユーザープロフィール</h2>
<p>名前: {user.name}</p>
<p>メールアドレス: {user.email}</p>
</div>
);
});
const UserSettings = memo(function UserSettings() {
const { user, updateSettings } = useContext(UserContext);
console.log('UserSettings rendered'); // デバッグ用
return (
<div>
<h2>設定</h2>
<label>
<input
type="checkbox"
checked={user.settings.notifications}
onChange={(e) => updateSettings({ notifications: e.target.checked })}
/>
通知を受け取る
</label>
</div>
);
});
useMemo
でContextの値をメモ化することで、不要な再レンダリングを防げます。
memo
でコンポーネントをラップするのも効果的ですね。
Contextを分割する
関連する状態ごとにContextを分けると、さらに最適化できます。
// ユーザー情報のContext
const UserInfoContext = createContext();
function UserInfoProvider({ children }) {
const [userInfo, setUserInfo] = useState({
name: '田中太郎',
email: 'tanaka@example.com'
});
const value = useMemo(() => ({
userInfo,
setUserInfo
}), [userInfo]);
return (
<UserInfoContext.Provider value={value}>
{children}
</UserInfoContext.Provider>
);
}
// ユーザー設定のContext
const UserSettingsContext = createContext();
function UserSettingsProvider({ children }) {
const [userSettings, setUserSettings] = useState({
theme: 'light',
notifications: true,
language: 'ja'
});
const value = useMemo(() => ({
userSettings,
setUserSettings,
updateSettings: (updates) => {
setUserSettings(prev => ({ ...prev, ...updates }));
}
}), [userSettings]);
return (
<UserSettingsContext.Provider value={value}>
{children}
</UserSettingsContext.Provider>
);
}
// 分割されたContext を使用
function UserProfile() {
const { userInfo } = useContext(UserInfoContext);
// userSettings が変更されても、このコンポーネントは再レンダリングされない
return (
<div>
<h2>ユーザープロフィール</h2>
<p>名前: {userInfo.name}</p>
<p>メールアドレス: {userInfo.email}</p>
</div>
);
}
function UserSettings() {
const { userSettings, updateSettings } = useContext(UserSettingsContext);
// userInfo が変更されても、このコンポーネントは再レンダリングされない
return (
<div>
<h2>設定</h2>
<label>
<input
type="checkbox"
checked={userSettings.notifications}
onChange={(e) => updateSettings({ notifications: e.target.checked })}
/>
通知を受け取る
</label>
</div>
);
}
このように、関連する状態ごとにContextを分けることで、効率的な再レンダリングが実現できます。
よくある問題と解決方法
useContextでよく遭遇する問題と、その解決方法をまとめました。
Context が undefined になる問題
問題: useContextを使おうとすると、undefinedが返される
function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// または、デフォルト値を設定
const UserContext = createContext({
user: null,
setUser: () => {},
// デフォルト値を設定
});
この対策により、エラーの原因がすぐに分かるようになります。
過度な再レンダリング問題
問題: Contextの値が変わるたびに、すべてのコンポーネントが再レンダリングされる
// Context の値をメモ化
const contextValue = useMemo(() => ({
user,
setUser,
updateUser: (updates) => setUser(prev => ({ ...prev, ...updates }))
}), [user]);
// コンポーネントをメモ化
const UserProfile = memo(function UserProfile() {
const { user } = useUser();
return <div>{user.name}</div>;
});
メモ化を適切に使うことで、パフォーマンスが向上します。
Contextが複雑になりすぎる問題
問題: 1つのContextにたくさんの状態を詰め込みすぎる
// 機能ごとにContext を分割
function AppProviders({ children }) {
return (
<UserProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</UserProvider>
);
}
機能ごとにContextを分けることで、管理しやすくなります。
エラーハンドリングの不備
問題: エラーが発生した時の処理が不十分
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const handleError = (error) => {
setError(error);
console.error('User context error:', error);
};
const clearError = () => {
setError(null);
};
return (
<UserContext.Provider value={{
user,
setUser,
error,
clearError,
handleError
}}>
{children}
</UserContext.Provider>
);
}
エラーハンドリングを組み込むことで、より安定したアプリになります。
実用的なベストプラクティス
useContextを効果的に使うためのコツをまとめました。
1. カスタムフックを活用する
// ❌ 悪い例
function UserProfile() {
const context = useContext(UserContext);
if (!context) {
throw new Error('Provider not found');
}
return <div>{context.user.name}</div>;
}
// ✅ 良い例
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
function UserProfile() {
const { user } = useUser();
return <div>{user.name}</div>;
}
カスタムフックを使うことで、エラーハンドリングとコードの再利用性が向上します。
2. 適切な粒度でContextを分割する
// ❌ 悪い例(すべてを1つのContextに詰め込む)
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
// ... 多くの状態
}
// ✅ 良い例(関連する状態ごとに分割)
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
関連する機能ごとにContextを分けることで、管理しやすくなります。
3. TypeScriptでの型安全性
TypeScriptを使う場合は、型定義も忘れずに。
interface User {
id: string;
name: string;
email: string;
}
interface UserContextType {
user: User | null;
setUser: (user: User | null) => void;
updateUser: (updates: Partial<User>) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
function useUser(): UserContextType {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
型安全性により、開発時のエラーを防げます。
4. テストしやすい設計
// テスト用のモックProvider
function MockUserProvider({ children, mockUser }) {
const mockValue = {
user: mockUser,
setUser: jest.fn(),
updateUser: jest.fn()
};
return (
<UserContext.Provider value={mockValue}>
{children}
</UserContext.Provider>
);
}
// テストでの使用
test('UserProfile displays user name', () => {
const mockUser = { name: 'テストユーザー' };
render(
<MockUserProvider mockUser={mockUser}>
<UserProfile />
</MockUserProvider>
);
expect(screen.getByText('テストユーザー')).toBeInTheDocument();
});
テストしやすい設計にすることで、品質の高いアプリを作れます。
まとめ
React useContextを使ったグローバル状態管理について、基本から実践まで詳しく解説しました。
useContextの主なメリット
propsの受け渡しが不要になるので、コードがスッキリします。 状態管理が一箇所に集約されるので、保守性が向上します。 再利用性の高いコンポーネントが作れます。
効果的な活用のポイント
- カスタムフックの作成: 使いやすいAPIを提供する
- 適切な粒度での分割: 関連する状態ごとにContextを分ける
- パフォーマンスの最適化: メモ化と適切な再レンダリング制御
- エラーハンドリング: 堅牢な状態管理を実装する
学習の進め方
useContextの習得は、以下の順序で進めることをおすすめします。
- 基本的な使い方: createContext、Provider、useContextの基本
- カスタムフック: より使いやすいAPIの作成
- 実践的な例: 認証やテーマ管理システムの実装
- 複数Contextの管理: 複雑な状態管理の体系化
- パフォーマンス最適化: 効率的な状態管理の実現
useContextは、小規模から中規模のReactアプリケーションにおいて、とても効果的な状態管理ソリューションです。 ReduxやZustandなどの外部ライブラリを検討する前に、まずuseContextを試してみることをおすすめします。
大丈夫です! 最初は複雑に感じるかもしれませんが、実際に使ってみると思っているより簡単です。
ぜひ、実際のプロジェクトでuseContextを活用して、より効率的で保守性の高いアプリケーションを作ってみてください。