👁️

ReactやNext.jsでのステート管理ライブラリの比較

に公開
3

注意 : この記事はAIによるDeep researchの内容が含まれるため、一部ハルシネーションを含む場合がございます。また、勉強中の筆者の備忘録としてアウトプットしている記事になるため間違った情報も含まれる可能性があります。あくまで参考としていただき間違っている情報がある場合はご指摘いただけますと幸いです。

React・Next.js ステート管理ライブラリの比較

Reactアプリケーション及びNext.jsにおける主要な状態管理ライブラリを、実装コード、開発体験、パフォーマンス、使用場面の観点から詳しく比較します。ToDoアプリの例を通じて、それぞれのライブラリの特徴を理解していきましょう。

状態管理の種類

現代のReact・Next.jsアプリケーションでは、以下の2種類の状態管理を考慮する必要があります:

  • Client State(クライアント状態): UI状態、フォーム入力、モーダルの開閉など
  • Server State(サーバー状態): APIから取得したデータ、キャッシュ、同期状態など

Client State管理ライブラリ

1. Redux - 堅牢なステート管理の定番

Reduxは長年にわたってReactの状態管理における標準として君臨してきました。

主な特徴

  • すべての状態は単一のストアで管理
  • ストアは複数のリデューサー(状態のスライス)の組み合わせで構成
  • Redux Toolkitにより大幅にボイラープレートが削減

実装例

// スライスの定義
import { createSlice } from '@reduxjs/toolkit'

const todoSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      })
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    removeTodo: (state, action) => {
      return state.filter(todo => todo.id !== action.payload)
    }
  }
})

export const { addTodo, toggleTodo, removeTodo } = todoSlice.actions
export default todoSlice.reducer
// コンポーネントでの使用
import { useSelector, useDispatch } from 'react-redux'
import { addTodo, toggleTodo, removeTodo } from './todoSlice'

function TodoApp() {
  const todos = useSelector(state => state.todos)
  const dispatch = useDispatch()

  const handleAddTodo = (text) => {
    dispatch(addTodo(text))
  }

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          <span 
            onClick={() => dispatch(toggleTodo(todo.id))}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            {todo.text}
          </span>
          <button onClick={() => dispatch(removeTodo(todo.id))}>削除</button>
        </div>
      ))}
    </div>
  )
}

Redux ステート更新フロー

適用場面

推奨:大規模アプリケーション

  • 厳密な状態管理が必要
  • 複雑な状態ロジック
  • タイムトラベルデバッグが必要

非推奨:小規模アプリケーション

  • 過度に複雑
  • 学習コストが高い

2. Zustand - シンプルで強力

Zustandは、Reduxよりもシンプルなアプローチを採用し、ストア内にデータと更新関数を直接配置します。

主な特徴

  • リデューサーやアクションの概念なし
  • 最小限のボイラープレート
  • TypeScript完全サポート

実装例

import { create } from 'zustand'

const useTodoStore = create((set) => ({
  todos: [],
  
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, {
      id: Date.now(),
      text,
      completed: false
    }]
  })),
  
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  })),
  
  removeTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  }))
}))

// コンポーネントでの使用
function TodoApp() {
  const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore()

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          <span 
            onClick={() => toggleTodo(todo.id)}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            {todo.text}
          </span>
          <button onClick={() => removeTodo(todo.id)}>削除</button>
        </div>
      ))}
    </div>
  )
}

Next.jsでの使用例

// pages/todos.js (Pages Router)
import { useTodoStore } from '../stores/todoStore'

export default function TodosPage() {
  const { todos, addTodo } = useTodoStore()

  return (
    <div>
      <h1>My Todos</h1>
      <button onClick={() => addTodo('New Todo')}>
        Add Todo
      </button>
      {/* TodoList component */}
    </div>
  )
}

適用場面

推奨:小〜中規模プロジェクト

  • 迅速な開発が必要
  • シンプルな状態管理
  • 学習コストを抑えたい

3. Jotai - ボトムアップアプローチ

Jotaiは、個々のアトムから状態を構築するボトムアップアプローチを採用します。

主な特徴

  • アトム単位での状態管理
  • 高い柔軟性
  • 自動的な依存関係追跡

実装例

import { atom, useAtom, useSetAtom } from 'jotai'

// アトムの定義
const todosAtom = atom([])

// 派生アトム(読み書き可能)
const addTodoAtom = atom(
  null,
  (get, set, text) => {
    const todos = get(todosAtom)
    set(todosAtom, [...todos, {
      id: Date.now(),
      text,
      completed: false
    }])
  }
)

const toggleTodoAtom = atom(
  null,
  (get, set, id) => {
    const todos = get(todosAtom)
    set(todosAtom, todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ))
  }
)

// コンポーネントでの使用
function TodoApp() {
  const [todos] = useAtom(todosAtom)
  const addTodo = useSetAtom(addTodoAtom)
  const toggleTodo = useSetAtom(toggleTodoAtom)

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          <span onClick={() => toggleTodo(todo.id)}>
            {todo.text}
          </span>
        </div>
      ))}
    </div>
  )
}

Next.jsでのSSR対応

// _app.js
import { Provider } from 'jotai'

export default function App({ Component, pageProps }) {
  return (
    <Provider>
      <Component {...pageProps} />
    </Provider>
  )
}

// 初期値の設定
import { useHydrateAtoms } from 'jotai/utils'

function HydrateAtoms({ initialValues, children }) {
  useHydrateAtoms(initialValues)
  return children
}

// pages/todos.js
export default function TodosPage({ initialTodos }) {
  return (
    <HydrateAtoms initialValues={[[todosAtom, initialTodos]]}>
      <TodoApp />
    </HydrateAtoms>
  )
}

export async function getServerSideProps() {
  const initialTodos = await fetchTodos()
  return { props: { initialTodos } }
}

適用場面

推奨:小〜中規模プロジェクト

  • 細かい状態制御が必要
  • 状態の依存関係が複雑
  • コンポーネント間での状態共有

4. MobX - クラスベースアプローチ

MobXは、オブザーバブルパターンを使用したクラスベースの状態管理ソリューションです。

実装例

import { makeAutoObservable } from 'mobx'
import { observer } from 'mobx-react-lite'

class TodoStore {
  todos = []

  constructor() {
    makeAutoObservable(this)
  }

  addTodo(text) {
    this.todos.push({
      id: Date.now(),
      text,
      completed: false
    })
  }

  toggleTodo(id) {
    const todo = this.todos.find(todo => todo.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
}

const todoStore = new TodoStore()

// observerでラップが必要
const TodoApp = observer(() => {
  return (
    <div>
      {todoStore.todos.map(todo => (
        <div key={todo.id}>
          <span onClick={() => todoStore.toggleTodo(todo.id)}>
            {todo.text}
          </span>
        </div>
      ))}
    </div>
  )
})

5. Recoil - 開発終了

Server State管理ライブラリ

1. TanStack Query - 最強のサーバー状態管理

TanStack Query(旧React Query)は、サーバー状態管理のデファクトスタンダードです。

主な特徴

  • 強力なキャッシュ機能
  • 自動的なバックグラウンド更新
  • 楽観的更新サポート
  • オフライン対応

実装例

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

// カスタムフック
function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const response = await fetch('/api/todos')
      return response.json()
    },
    staleTime: 5 * 60 * 1000, // 5分間は再フェッチしない
  })
}

function useAddTodo() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (newTodo) => {
      const response = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodo),
      })
      return response.json()
    },
    onSuccess: () => {
      // キャッシュを無効化して再フェッチ
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

// コンポーネントでの使用
function TodoApp() {
  const { data: todos, isLoading, error } = useTodos()
  const addTodoMutation = useAddTodo()

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <div>
      <button 
        onClick={() => addTodoMutation.mutate({ text: 'New Todo' })}
        disabled={addTodoMutation.isPending}
      >
        {addTodoMutation.isPending ? 'Adding...' : 'Add Todo'}
      </button>
      
      {todos?.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

Next.jsでのSSR/SSG対応

// pages/todos.js
import { dehydrate, QueryClient } from '@tanstack/react-query'

export default function TodosPage() {
  return <TodoApp />
}

export async function getServerSideProps() {
  const queryClient = new QueryClient()

  // サーバーサイドでプリフェッチ
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

// _app.js
import { QueryClient, QueryClientProvider, Hydrate } from '@tanstack/react-query'
import { useState } from 'react'

export default function App({ Component, pageProps }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}

App Routerでの使用

// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export default function Providers({ children }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

// app/layout.tsx
import Providers from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

// app/todos/page.tsx
'use client'
import { useTodos } from '../hooks/useTodos'

export default function TodosPage() {
  const { data: todos, isLoading } = useTodos()
  
  if (isLoading) return <div>Loading...</div>
  
  return (
    <div>
      {todos?.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

2. SWR - 軽量なサーバー状態管理

SWRは、Vercel(Next.jsの開発元)が開発した軽量なデータフェッチライブラリです。

主な特徴

  • Next.jsとの優秀な統合
  • シンプルなAPI
  • 小さなバンドルサイズ
  • ストイック・ワイル・リバリデート戦略

実装例

import useSWR, { mutate } from 'swr'

const fetcher = (url) => fetch(url).then(res => res.json())

function useTodos() {
  const { data, error, isLoading } = useSWR('/api/todos', fetcher, {
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
  })

  return {
    todos: data,
    isLoading,
    error
  }
}

async function addTodo(newTodo) {
  // 楽観的更新
  mutate('/api/todos', async (todos) => {
    return [...todos, { ...newTodo, id: Date.now() }]
  }, false)

  // サーバーに送信
  await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  })

  // 再検証
  mutate('/api/todos')
}

function TodoApp() {
  const { todos, isLoading, error } = useTodos()

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error loading todos</div>

  return (
    <div>
      <button onClick={() => addTodo({ text: 'New Todo' })}>
        Add Todo
      </button>
      {todos?.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

Next.jsでのSSR対応

// pages/todos.js
import { SWRConfig } from 'swr'

export default function TodosPage({ fallback }) {
  return (
    <SWRConfig value={{ fallback }}>
      <TodoApp />
    </SWRConfig>
  )
}

export async function getServerSideProps() {
  const todos = await fetchTodos()
  
  return {
    props: {
      fallback: {
        '/api/todos': todos
      }
    }
  }
}

3. RTK Query - Redux生態系のサーバー状態管理

RTK Queryは、Redux Toolkitに含まれるサーバー状態管理ソリューションです。

実装例

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const todosApi = createApi({
  reducerPath: 'todosApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Todo'],
  endpoints: (builder) => ({
    getTodos: builder.query({
      query: () => 'todos',
      providesTags: ['Todo'],
    }),
    addTodo: builder.mutation({
      query: (newTodo) => ({
        url: 'todos',
        method: 'POST',
        body: newTodo,
      }),
      invalidatesTags: ['Todo'],
    }),
  }),
})

export const { useGetTodosQuery, useAddTodoMutation } = todosApi

function TodoApp() {
  const { data: todos, isLoading } = useGetTodosQuery()
  const [addTodo, { isLoading: isAdding }] = useAddTodoMutation()

  return (
    <div>
      <button 
        onClick={() => addTodo({ text: 'New Todo' })}
        disabled={isAdding}
      >
        Add Todo
      </button>
      {todos?.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

Next.js特有の状態管理パターン

Server Components + Client Components

App Routerでは、Server ComponentsとClient Componentsを組み合わせた効率的な状態管理が可能です。

// app/todos/page.tsx (Server Component)
import { TodoList } from './TodoList'
import { getTodos } from '@/lib/api'

export default async function TodosPage() {
  const initialTodos = await getTodos()

  return (
    <div>
      <h1>Todos</h1>
      <TodoList initialTodos={initialTodos} />
    </div>
  )
}

// app/todos/TodoList.tsx (Client Component)
'use client'
import { useState } from 'react'

export function TodoList({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos)

  const addTodo = (text) => {
    setTodos(prev => [...prev, { id: Date.now(), text, completed: false }])
  }

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

Parallel Data Fetching

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { getTodos } from '@/lib/api'
import { getUsers } from '@/lib/api'

// 並列でデータフェッチ
const todosPromise = getTodos()
const usersPromise = getUsers()

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<div>Loading todos...</div>}>
        <TodoSection todosPromise={todosPromise} />
      </Suspense>
      
      <Suspense fallback={<div>Loading users...</div>}>
        <UserSection usersPromise={usersPromise} />
      </Suspense>
    </div>
  )
}

ライブラリ比較まとめ

Client State管理

ライブラリ アプローチ 主要な概念 推奨プロジェクトサイズ Next.js統合 特徴・注意点
Redux トップダウン ストア、リデューサー、アクション 大規模 ⭐⭐⭐ 堅牢で厳格。Redux Toolkitで簡素化。学習曲線急。
Zustand トップダウン ストア、更新関数 小〜中規模 ⭐⭐⭐⭐⭐ シンプルで軽量。SSR対応良好。
Jotai ボトムアップ アトム 小〜中規模 ⭐⭐⭐⭐ 柔軟性高い。SSR設定やや複雑。
MobX クラスベース オブザーバブル 特定用途 ⭐⭐ 関数型パラダイムと異なる。
Recoil ボトムアップ アトム 使用非推奨 開発終了、移行推奨。

Server State管理

ライブラリ キャッシュ Next.js統合 学習コスト エコシステム 推奨用途
TanStack Query ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 複雑なサーバー状態管理
SWR ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ Next.jsプロジェクト
RTK Query ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ Redux使用プロジェクト

選択指針

🏢 大規模アプリケーション

  • Client State: Redux + Redux Toolkit
  • Server State: TanStack Query
  • チーム開発、厳密な管理が必要

🚀 中小規模アプリケーション

  • Client State: Zustand or Jotai
  • Server State: SWR or TanStack Query
  • 開発速度重視

⚡ Next.jsプロジェクト

  • Client State: Zustand(SSR対応良好)
  • Server State: SWR(Next.js最適化)
  • Vercelエコシステム活用

🎯 特殊要件

  • 既存Redux: RTK Query
  • 複雑な状態依存: Jotai
  • ゼロ配信: React内蔵hooks

まとめ

2025年現在、状態管理は Client StateServer State を分けて考えることが重要です。

Next.jsのServer Componentsを活用することで、多くの状態管理を簡素化できるため、まずはServer Componentsでの解決を検討し、必要に応じてClient State管理ライブラリを導入しましょう。

Discussion

Honey32Honey32

失礼します。

大規模アプリケーションで Jotai よりも Redux を推奨するのはなぜですか?

Jotai は「ボトムアップ」ですが、このようなアプローチはむしろスケールしやすいです。

実際に僕も業務で使っていますが、アプリケーションの規模が大きくなっても問題なく活用できていることが確認できています。

れんれん

貴重なご意見いただきありがとうございます。
勉強になります。
別記事においてもご返信させていただきましたが、私がまだまだ勉強中という前提にはなりますが、JotaiとReduxを大規模アプリケーションで採用する推奨指針を私なりに調べたので共有させていただきます。
間違っている点や考慮漏れ、その他アドバイスありましたらいただけますと幸いです。

大規模アプリケーションでの選択

Jotaiを推奨する場合:

  • 機能間の独立性を重視
  • チームの並行開発が多い
  • パフォーマンスの細かい制御が必要
  • 状態の局所性が高い

Reduxを推奨する場合:

  • 厳密な状態管理フローが必要(金融系など)
  • タイムトラベルデバッギングが必須
  • 既存のReduxエコシステムとの統合
  • 企業の標準化要件
Honey32Honey32

はい、(Redux をフルに活用した経験が薄いので、あまりハッキリとは言えませんが、)そんな感じになると思います!