概要
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/useDispatchにRootState/AppDispatch型を渡すことで型推論が効く- App Router では
Providerを"use client"のラッパーコンポーネントに分離する必要がある(Server Component に直接置けない)