レビュー済み·難易度: 中級·更新: 2026-04-16

Redux Toolkit の createSlice でリデューサーとアクションをまとめる

createSlice でリデューサーとアクションを一括定義し、useSelector / useDispatch でコンポーネントから状態を読み書きする実装例。

nextjsstate-managementredux-toolkit

対応バージョン

nextjs 15react 19redux-toolkit 2

前提環境

React の useState の基本と、状態管理ライブラリの概念を理解していること

概要

createSlice を使うと、リデューサーとアクションクリエイターを一箇所にまとめて定義できる。 useSelector で状態を読み取り、useDispatch でアクションを発行する基本パターンを示す。

インストール

npm install @reduxjs/toolkit react-redux

slice 定義

// src/store/todoSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

type TodoState = {
  items: Todo[];
};

const initialState: TodoState = {
  items: [],
};

export const todoSlice = createSlice({
  name: "todo",
  initialState,
  reducers: {
    addTodo: (state, action: PayloadAction<string>) => {
      state.items.push({
        id: Date.now(),
        text: action.payload,
        completed: false,
      });
    },
    toggleTodo: (state, action: PayloadAction<number>) => {
      const todo = state.items.find((t) => t.id === action.payload);
      if (todo) todo.completed = !todo.completed;
    },
    removeTodo: (state, action: PayloadAction<number>) => {
      state.items = state.items.filter((t) => t.id !== action.payload);
    },
  },
});

export const { addTodo, toggleTodo, removeTodo } = todoSlice.actions;
export default todoSlice.reducer;

store 設定

// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./todoSlice";

export const store = configureStore({
  reducer: {
    todo: todoReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Provider 設定(App Router 用)

// src/components/providers/ReduxProvider.tsx
"use client";

import { Provider } from "react-redux";
import { store } from "@/store";

export function ReduxProvider({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}
// src/app/layout.tsx
import { ReduxProvider } from "@/components/providers/ReduxProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <ReduxProvider>{children}</ReduxProvider>
      </body>
    </html>
  );
}

コンポーネントでの使用

// src/components/TodoApp.tsx
"use client";

import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addTodo, toggleTodo, removeTodo } from "@/store/todoSlice";
import type { RootState, AppDispatch } from "@/store";

export function TodoApp() {
  const [text, setText] = useState("");
  const items = useSelector((state: RootState) => state.todo.items);
  const dispatch = useDispatch<AppDispatch>();

  return (
    <div className="space-y-4">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (text.trim()) {
            dispatch(addTodo(text.trim()));
            setText("");
          }
        }}
        className="flex gap-2"
      >
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          className="rounded border px-3 py-2 text-sm"
          placeholder="タスクを入力"
        />
        <button
          type="submit"
          className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
        >
          追加
        </button>
      </form>

      <ul className="space-y-2">
        {items.map((todo) => (
          <li key={todo.id} className="flex items-center gap-3 rounded border px-4 py-2 text-sm">
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch(toggleTodo(todo.id))}
            />
            <span className={todo.completed ? "line-through text-gray-400" : ""}>{todo.text}</span>
            <button
              onClick={() => dispatch(removeTodo(todo.id))}
              className="ml-auto text-red-500 hover:underline"
            >
              削除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

ポイント

  • createSlice 内のリデューサーは Immer が組み込まれているため、state.items.push(...) のようにミュータブルに書ける
  • PayloadAction<T> でアクションのペイロード型を明示する
  • useSelector / useDispatchRootState / AppDispatch 型を渡すことで型推論が効く
  • App Router では Provider"use client" のラッパーコンポーネントに分離する必要がある(Server Component に直接置けない)

注意点

Next.js App Router では Redux Provider をクライアントコンポーネントとして設定する必要がある。Zustand と比べるとボイラープレートが多いが、DevTools による状態の可視化や大規模アプリでの予測可能性が強み。

関連サンプル