開発/11 min read/February 5, 2025
Danylo Kabanov
Chief Technology Officer
#Reactの状態管理:2025年版ガイド
React アプリケーションが成長するにつれて、効果的な状態管理がますます重要になります。2025年、選択肢は豊富です。各アプローチのメリットとユースケースを見ていきましょう。
##Reactの組み込みフック
小規模から中規模のアプリケーションには、Reactの組み込みフックで十分です。
###useState と useReducer
tsx
1import { useState, useReducer } from 'react'23// シンプルな状態管理4function Counter() {5 const [count, setCount] = useState(0)67 return (8 <button onClick={() => setCount(count + 1)}>9 カウント: {count}10 </button>11 )12}1314// 複雑な状態管理15type Action =16 | { type: 'increment' }17 | { type: 'decrement' }18 | { type: 'reset' }1920function counterReducer(state: number, action: Action): number {21 switch (action.type) {22 case 'increment':23 return state + 124 case 'decrement':25 return state - 126 case 'reset':27 return 028 default:29 return state30 }31}3233function AdvancedCounter() {34 const [count, dispatch] = useReducer(counterReducer, 0)3536 return (37 <div>38 <p>カウント: {count}</p>39 <button onClick={() => dispatch({ type: 'increment' })}>+</button>40 <button onClick={() => dispatch({ type: 'decrement' })}>-</button>41 <button onClick={() => dispatch({ type: 'reset' })}>リセット</button>42 </div>43 )44}
###Context API
グローバル状態を共有する場合に使用します。
tsx
1import { createContext, useContext, useState, ReactNode } from 'react'23interface ThemeContextType {4 theme: 'light' | 'dark'5 toggleTheme: () => void6}78const ThemeContext = createContext<ThemeContextType | undefined>(undefined)910export function ThemeProvider({ children }: { children: ReactNode }) {11 const [theme, setTheme] = useState<'light' | 'dark'>('light')1213 const toggleTheme = () => {14 setTheme(prev => prev === 'light' ? 'dark' : 'light')15 }1617 return (18 <ThemeContext.Provider value={{ theme, toggleTheme }}>19 {children}20 </ThemeContext.Provider>21 )22}2324export function useTheme() {25 const context = useContext(ThemeContext)26 if (!context) {27 throw new Error('useTheme must be used within ThemeProvider')28 }29 return context30}
##Zustand - 軽量で強力
シンプルで型安全な状態管理が必要な場合、Zustand は優れた選択肢です。
tsx
1import { create } from 'zustand'2import { devtools, persist } from 'zustand/middleware'34interface UserStore {5 user: User | null6 isAuthenticated: boolean7 login: (user: User) => void8 logout: () => void9}1011export const useUserStore = create<UserStore>()(12 devtools(13 persist(14 (set) => ({15 user: null,16 isAuthenticated: false,17 login: (user) => set({ user, isAuthenticated: true }),18 logout: () => set({ user: null, isAuthenticated: false }),19 }),20 {21 name: 'user-storage',22 }23 )24 )25)2627// コンポーネントでの使用28function UserProfile() {29 const { user, logout } = useUserStore()3031 if (!user) return <div>ログインしていません</div>3233 return (34 <div>35 <h2>{user.name}</h2>36 <button onClick={logout}>ログアウト</button>37 </div>38 )39}4041// セレクターで最適化42function UserName() {43 const name = useUserStore(state => state.user?.name)44 return <span>{name}</span>45}
##TanStack Query - サーバー状態管理
API からのデータを扱う場合、TanStack Query(旧 React Query)が最適です。
tsx
1import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'23// データの取得4function UsersList() {5 const { data, isLoading, error } = useQuery({6 queryKey: ['users'],7 queryFn: fetchUsers,8 staleTime: 5 * 60 * 1000, // 5分間はキャッシュを使用9 })1011 if (isLoading) return <div>読み込み中...</div>12 if (error) return <div>エラー: {error.message}</div>1314 return (15 <ul>16 {data.map(user => (17 <li key={user.id}>{user.name}</li>18 ))}19 </ul>20 )21}2223// データの更新24function CreateUserForm() {25 const queryClient = useQueryClient()2627 const mutation = useMutation({28 mutationFn: createUser,29 onSuccess: () => {30 // キャッシュを無効化して再取得31 queryClient.invalidateQueries({ queryKey: ['users'] })32 },33 })3435 const handleSubmit = (data: CreateUserInput) => {36 mutation.mutate(data)37 }3839 return (40 <form onSubmit={handleSubmit}>41 {/* フォームフィールド */}42 <button type="submit" disabled={mutation.isPending}>43 {mutation.isPending ? '作成中...' : 'ユーザーを作成'}44 </button>45 </form>46 )47}4849// 楽観的更新50function UpdateUserButton({ userId }: { userId: string }) {51 const queryClient = useQueryClient()5253 const mutation = useMutation({54 mutationFn: updateUser,55 onMutate: async (updatedUser) => {56 // 進行中のクエリをキャンセル57 await queryClient.cancelQueries({ queryKey: ['users', userId] })5859 // 以前のデータを保存60 const previousUser = queryClient.getQueryData(['users', userId])6162 // 楽観的に更新63 queryClient.setQueryData(['users', userId], updatedUser)6465 return { previousUser }66 },67 onError: (err, updatedUser, context) => {68 // エラー時はロールバック69 queryClient.setQueryData(['users', userId], context?.previousUser)70 },71 })7273 return (74 <button onClick={() => mutation.mutate({ id: userId, active: true })}>75 有効化76 </button>77 )78}
##Jotai - アトミックな状態管理
細かい粒度での状態管理が必要な場合、Jotai が便利です。
tsx
1import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'23// プリミティブアトム4const countAtom = atom(0)5const nameAtom = atom('CyberWolf')67// 派生アトム8const doubleCountAtom = atom((get) => get(countAtom) * 2)910// 書き込み可能な派生アトム11const incrementAtom = atom(12 (get) => get(countAtom),13 (get, set) => set(countAtom, get(countAtom) + 1)14)1516// コンポーネントでの使用17function Counter() {18 const [count, setCount] = useAtom(countAtom)19 const doubleCount = useAtomValue(doubleCountAtom)20 const increment = useSetAtom(incrementAtom)2122 return (23 <div>24 <p>カウント: {count}</p>25 <p>2倍: {doubleCount}</p>26 <button onClick={increment}>インクリメント</button>27 </div>28 )29}3031// 非同期アトム32const userAtom = atom(async (get) => {33 const userId = get(userIdAtom)34 const response = await fetch(`/api/users/${userId}`)35 return response.json()36})3738function UserProfile() {39 const user = useAtomValue(userAtom)40 return <div>{user.name}</div>41}
##選択のガイドライン
どの状態管理ソリューションを選ぶべきか?
###useState/useReducer を使用する場合
- ✅ ローカルコンポーネント状態
- ✅ シンプルなフォーム状態
- ✅ UI状態(モーダル、ドロップダウンなど)
###Context API を使用する場合
- ✅ テーマ、言語などのグローバル設定
- ✅ 認証状態
- ✅ 更新頻度が低いグローバル状態
###Zustand を使用する場合
- ✅ クライアント側のグローバル状態
- ✅ ミドルウェアが必要な場合
- ✅ Redux より軽量なソリューションが欲しい場合
###TanStack Query を使用する場合
- ✅ サーバーからのデータ取得
- ✅ キャッシング、再検証が必要な場合
- ✅ バックグラウンド更新
- ✅ 楽観的更新
###Jotai を使用する場合
- ✅ 細かい粒度の状態管理
- ✅ 派生状態が多い場合
- ✅ 原子的な更新が必要な場合
##組み合わせて使用する
実際のアプリケーションでは、複数のソリューションを組み合わせることが一般的です。
tsx
1import { useUserStore } from '@/stores/user'2import { useQuery } from '@tanstack/react-query'3import { useAtom } from 'jotai'4import { themeAtom } from '@/atoms/theme'56function Dashboard() {7 // グローバルクライアント状態(Zustand)8 const { user } = useUserStore()910 // サーバー状態(TanStack Query)11 const { data: stats } = useQuery({12 queryKey: ['stats', user.id],13 queryFn: () => fetchUserStats(user.id),14 })1516 // UI状態(Jotai)17 const [theme] = useAtom(themeAtom)1819 return (20 <div className={theme}>21 <h1>{user.name}のダッシュボード</h1>22 <Stats data={stats} />23 </div>24 )25}
##パフォーマンス最適化
状態管理のパフォーマンスを最適化するためのヒント:
tsx
1// 1. セレクターを使用して不要な再レンダリングを防ぐ2const name = useUserStore(state => state.user?.name)34// 2. React.memo で高価なコンポーネントをメモ化5const MemoizedList = React.memo(UserList)67// 3. useMemo で計算結果をキャッシュ8const sortedUsers = useMemo(9 () => users.sort((a, b) => a.name.localeCompare(b.name)),10 [users]11)1213// 4. useCallback でコールバックを安定させる14const handleClick = useCallback(() => {15 dispatch({ type: 'increment' })16}, [])
##結論
2025年のReactエコシステムは、状態管理のための豊富な選択肢を提供しています。各ソリューションには独自の強みがあり、プロジェクトの要件に応じて適切なものを選択することが重要です。
***
React アプリケーションのアーキテクチャについてサポートが必要ですか?お問い合わせください。
React状態管理フロントエンド