状態管理 レッスン3
Redux Toolkit
大規模アプリケーションのための堅牢な状態管理を学ぼう
configureStoreでストアを作成
Redux Toolkit(RTK)は、Reduxの公式推奨ツールキットです。 従来のReduxに比べてボイラープレートが大幅に削減されています。 まずconfigureStoreでストアを作成します。
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counter/counterSlice";
import todosReducer from "./features/todos/todosSlice";
import userReducer from "./features/user/userSlice";
// ストアの作成
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
user: userReducer,
},
});
// 型定義(TypeScript)
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// アプリのルートでProviderを設置
// app.tsx
import { Provider } from "react-redux";
import { store } from "./store";
function App() {
return (
<Provider store={store}>
<MainApp />
</Provider>
);
}configureStoreは Redux DevTools、thunkミドルウェア、immutabilityチェックなどを自動で設定してくれます。
createSliceで状態と更新ロジックを定義
createSliceは、状態の初期値、更新ロジック(reducer)、 アクション生成関数をまとめて定義できる便利な関数です。
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface CounterState {
value: number;
history: number[];
}
const initialState: CounterState = {
value: 0,
history: [],
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
// Immerが内部で使われているので、直接stateを変更できる
increment(state) {
state.value += 1;
state.history.push(state.value);
},
decrement(state) {
state.value -= 1;
state.history.push(state.value);
},
// PayloadAction で引数の型を指定
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload;
state.history.push(state.value);
},
reset(state) {
state.value = 0;
state.history = [];
},
},
});
// アクション生成関数をエクスポート
export const { increment, decrement, incrementByAmount, reset } =
counterSlice.actions;
// reducerをエクスポート
export default counterSlice.reducer;RTKでは内部的にImmerが使われているため、state.value += 1のように 直接変更する書き方が可能です(実際にはイミュータブルに処理されます)。
useSelectorとuseDispatch
コンポーネントからReduxストアにアクセスするには、useSelector(値の取得)とuseDispatch(アクションの実行)を使います。
import { useSelector, useDispatch } from "react-redux";
import type { RootState, AppDispatch } from "../../store";
import { increment, decrement, incrementByAmount, reset } from "./counterSlice";
// 型付きフック(推奨:hooks.tsに定義しておく)
import { useAppSelector, useAppDispatch } from "../../hooks";
// hooks.ts:
// export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// export const useAppDispatch: () => AppDispatch = useDispatch;
function Counter() {
// ストアから値を取得
const count = useAppSelector((state) => state.counter.value);
const history = useAppSelector((state) => state.counter.history);
// アクションをディスパッチする関数を取得
const dispatch = useAppDispatch();
return (
<div>
<h2>カウント: {count}</h2>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(incrementByAmount(10))}>+10</button>
<button onClick={() => dispatch(reset())}>リセット</button>
<h3>履歴</h3>
<ul>
{history.map((val, i) => (
<li key={i}>{val}</li>
))}
</ul>
</div>
);
}useSelectorは必要な部分だけを選択するので、 関係ない状態が変わっても再レンダリングされません。
非同期処理(createAsyncThunk)
API呼び出しなどの非同期処理はcreateAsyncThunkで定義します。 ローディング状態やエラー処理も統一的に管理できます。
// features/todos/todosSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
interface Todo {
id: number;
title: string;
completed: boolean;
}
interface TodosState {
items: Todo[];
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
}
const initialState: TodosState = {
items: [],
status: "idle",
error: null,
};
// 非同期アクションを定義
export const fetchTodos = createAsyncThunk(
"todos/fetchTodos",
async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos?_limit=10");
return (await response.json()) as Todo[];
}
);
export const addTodo = createAsyncThunk(
"todos/addTodo",
async (title: string) => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, completed: false }),
});
return (await response.json()) as Todo;
}
);
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
toggleTodo(state, action: PayloadAction<number>) {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
},
// 非同期アクションの結果を処理
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = "loading";
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message ?? "Unknown error";
})
.addCase(addTodo.fulfilled, (state, action) => {
state.items.push(action.payload);
});
},
});
export const { toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;RTK Query の基本
RTK QueryはRedux Toolkitに組み込まれたデータフェッチング・キャッシュ管理ツールです。 API呼び出しのボイラープレートを大幅に削減できます。
// services/api.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// APIの定義
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com",
}),
tagTypes: ["Post"],
endpoints: (builder) => ({
// GET /posts
getPosts: builder.query<Post[], void>({
query: () => "/posts",
providesTags: ["Post"],
}),
// GET /posts/:id
getPost: builder.query<Post, number>({
query: (id) => `/posts/${id}`,
}),
// POST /posts
addPost: builder.mutation<Post, Partial<Post>>({
query: (newPost) => ({
url: "/posts",
method: "POST",
body: newPost,
}),
invalidatesTags: ["Post"], // キャッシュを自動で無効化
}),
}),
});
// 自動生成されたフック
export const { useGetPostsQuery, useGetPostQuery, useAddPostMutation } = postsApi;
// コンポーネントで使う
function PostsList() {
const { data: posts, isLoading, error } = useGetPostsQuery();
if (isLoading) return <p>読み込み中...</p>;
if (error) return <p>エラーが発生しました</p>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}RTK Queryはキャッシュ、再フェッチ、ポーリング、楽観的更新などの機能を標準で備えています。createAsyncThunkよりもAPI通信に特化した選択肢です。
まとめ
configureStoreでストアを作成し、DevToolsやミドルウェアが自動設定されるcreateSliceで状態・reducer・アクションをまとめて定義するuseSelectorで値を取得、useDispatchでアクションを実行するcreateAsyncThunkで非同期処理とローディング状態を管理するRTK QueryはAPI通信のキャッシュ管理に特化した強力なツール