開発

Reactの状態管理:2025年版ガイド

cyberwolf.studio
開発/11 min read/February 5, 2025
Danylo Kabanov
Danylo Kabanov
Chief Technology Officer

#Reactの状態管理:2025年版ガイド

React アプリケーションが成長するにつれて、効果的な状態管理がますます重要になります。2025年、選択肢は豊富です。各アプローチのメリットとユースケースを見ていきましょう。

##Reactの組み込みフック

小規模から中規模のアプリケーションには、Reactの組み込みフックで十分です。

###useState と useReducer

tsx
1import { useState, useReducer } from 'react'
2
3// シンプルな状態管理
4function Counter() {
5 const [count, setCount] = useState(0)
6
7 return (
8 <button onClick={() => setCount(count + 1)}>
9 カウント: {count}
10 </button>
11 )
12}
13
14// 複雑な状態管理
15type Action =
16 | { type: 'increment' }
17 | { type: 'decrement' }
18 | { type: 'reset' }
19
20function counterReducer(state: number, action: Action): number {
21 switch (action.type) {
22 case 'increment':
23 return state + 1
24 case 'decrement':
25 return state - 1
26 case 'reset':
27 return 0
28 default:
29 return state
30 }
31}
32
33function AdvancedCounter() {
34 const [count, dispatch] = useReducer(counterReducer, 0)
35
36 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'
2
3interface ThemeContextType {
4 theme: 'light' | 'dark'
5 toggleTheme: () => void
6}
7
8const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
9
10export function ThemeProvider({ children }: { children: ReactNode }) {
11 const [theme, setTheme] = useState<'light' | 'dark'>('light')
12
13 const toggleTheme = () => {
14 setTheme(prev => prev === 'light' ? 'dark' : 'light')
15 }
16
17 return (
18 <ThemeContext.Provider value={{ theme, toggleTheme }}>
19 {children}
20 </ThemeContext.Provider>
21 )
22}
23
24export function useTheme() {
25 const context = useContext(ThemeContext)
26 if (!context) {
27 throw new Error('useTheme must be used within ThemeProvider')
28 }
29 return context
30}

##Zustand - 軽量で強力

シンプルで型安全な状態管理が必要な場合、Zustand は優れた選択肢です。

tsx
1import { create } from 'zustand'
2import { devtools, persist } from 'zustand/middleware'
3
4interface UserStore {
5 user: User | null
6 isAuthenticated: boolean
7 login: (user: User) => void
8 logout: () => void
9}
10
11export 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)
26
27// コンポーネントでの使用
28function UserProfile() {
29 const { user, logout } = useUserStore()
30
31 if (!user) return <div>ログインしていません</div>
32
33 return (
34 <div>
35 <h2>{user.name}</h2>
36 <button onClick={logout}>ログアウト</button>
37 </div>
38 )
39}
40
41// セレクターで最適化
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'
2
3// データの取得
4function UsersList() {
5 const { data, isLoading, error } = useQuery({
6 queryKey: ['users'],
7 queryFn: fetchUsers,
8 staleTime: 5 * 60 * 1000, // 5分間はキャッシュを使用
9 })
10
11 if (isLoading) return <div>読み込み中...</div>
12 if (error) return <div>エラー: {error.message}</div>
13
14 return (
15 <ul>
16 {data.map(user => (
17 <li key={user.id}>{user.name}</li>
18 ))}
19 </ul>
20 )
21}
22
23// データの更新
24function CreateUserForm() {
25 const queryClient = useQueryClient()
26
27 const mutation = useMutation({
28 mutationFn: createUser,
29 onSuccess: () => {
30 // キャッシュを無効化して再取得
31 queryClient.invalidateQueries({ queryKey: ['users'] })
32 },
33 })
34
35 const handleSubmit = (data: CreateUserInput) => {
36 mutation.mutate(data)
37 }
38
39 return (
40 <form onSubmit={handleSubmit}>
41 {/* フォームフィールド */}
42 <button type="submit" disabled={mutation.isPending}>
43 {mutation.isPending ? '作成中...' : 'ユーザーを作成'}
44 </button>
45 </form>
46 )
47}
48
49// 楽観的更新
50function UpdateUserButton({ userId }: { userId: string }) {
51 const queryClient = useQueryClient()
52
53 const mutation = useMutation({
54 mutationFn: updateUser,
55 onMutate: async (updatedUser) => {
56 // 進行中のクエリをキャンセル
57 await queryClient.cancelQueries({ queryKey: ['users', userId] })
58
59 // 以前のデータを保存
60 const previousUser = queryClient.getQueryData(['users', userId])
61
62 // 楽観的に更新
63 queryClient.setQueryData(['users', userId], updatedUser)
64
65 return { previousUser }
66 },
67 onError: (err, updatedUser, context) => {
68 // エラー時はロールバック
69 queryClient.setQueryData(['users', userId], context?.previousUser)
70 },
71 })
72
73 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'
2
3// プリミティブアトム
4const countAtom = atom(0)
5const nameAtom = atom('CyberWolf')
6
7// 派生アトム
8const doubleCountAtom = atom((get) => get(countAtom) * 2)
9
10// 書き込み可能な派生アトム
11const incrementAtom = atom(
12 (get) => get(countAtom),
13 (get, set) => set(countAtom, get(countAtom) + 1)
14)
15
16// コンポーネントでの使用
17function Counter() {
18 const [count, setCount] = useAtom(countAtom)
19 const doubleCount = useAtomValue(doubleCountAtom)
20 const increment = useSetAtom(incrementAtom)
21
22 return (
23 <div>
24 <p>カウント: {count}</p>
25 <p>2倍: {doubleCount}</p>
26 <button onClick={increment}>インクリメント</button>
27 </div>
28 )
29}
30
31// 非同期アトム
32const userAtom = atom(async (get) => {
33 const userId = get(userIdAtom)
34 const response = await fetch(`/api/users/${userId}`)
35 return response.json()
36})
37
38function 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'
5
6function Dashboard() {
7 // グローバルクライアント状態(Zustand)
8 const { user } = useUserStore()
9
10 // サーバー状態(TanStack Query)
11 const { data: stats } = useQuery({
12 queryKey: ['stats', user.id],
13 queryFn: () => fetchUserStats(user.id),
14 })
15
16 // UI状態(Jotai)
17 const [theme] = useAtom(themeAtom)
18
19 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)
3
4// 2. React.memo で高価なコンポーネントをメモ化
5const MemoizedList = React.memo(UserList)
6
7// 3. useMemo で計算結果をキャッシュ
8const sortedUsers = useMemo(
9 () => users.sort((a, b) => a.name.localeCompare(b.name)),
10 [users]
11)
12
13// 4. useCallback でコールバックを安定させる
14const handleClick = useCallback(() => {
15 dispatch({ type: 'increment' })
16}, [])

##結論

2025年のReactエコシステムは、状態管理のための豊富な選択肢を提供しています。各ソリューションには独自の強みがあり、プロジェクトの要件に応じて適切なものを選択することが重要です。

***

React アプリケーションのアーキテクチャについてサポートが必要ですか?お問い合わせください。

React状態管理フロントエンド