概要
createAsyncThunk を使うと、API フェッチのような非同期処理を pending / fulfilled / rejected の3状態で管理できる。
extraReducers で各状態に対応する reducer を定義し、ローディング・成功・エラーを slice の state に反映する。
インストール
npm install @reduxjs/toolkit react-redux
実装例
// src/store/postsSlice.ts
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
type Post = { id: number; title: string; body: string };
type PostsState = {
items: Post[];
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
};
const initialState: PostsState = {
items: [],
status: "idle",
error: null,
};
// 非同期処理を定義する
export const fetchPosts = createAsyncThunk(
"posts/fetchAll",
async (_, { rejectWithValue }) => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
if (!res.ok) {
return rejectWithValue(`HTTP error: ${res.status}`);
}
return (await res.json()) as Post[];
}
);
export const postsSlice = createSlice({
name: "posts",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = "loading";
state.error = null;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
});
},
});
export default postsSlice.reducer;
// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "./postsSlice";
export const store = configureStore({
reducer: {
posts: postsReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// src/components/PostList.tsx
"use client";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchPosts } from "@/store/postsSlice";
import type { AppDispatch, RootState } from "@/store";
export function PostList() {
const dispatch = useDispatch<AppDispatch>();
const { items, status, error } = useSelector((state: RootState) => state.posts);
useEffect(() => {
if (status === "idle") {
dispatch(fetchPosts());
}
}, [dispatch, status]);
if (status === "loading") return <p className="text-sm text-gray-400">読み込み中...</p>;
if (status === "failed") return <p className="text-sm text-red-600">{error}</p>;
return (
<ul className="space-y-2">
{items.map((post) => (
<li key={post.id} className="rounded border px-4 py-2 text-sm">
<p className="font-medium">{post.title}</p>
<p className="text-gray-500">{post.body}</p>
</li>
))}
</ul>
);
}
ポイント
createAsyncThunkの第1引数はアクション名("スライス名/アクション名"の形式が慣例)rejectWithValueでエラーペイロードを型付きで返せる(action.payloadで受け取れる)extraReducersのbuilder.addCaseでpending/fulfilled/rejectedを個別に処理するstatus === "idle"のときだけ dispatch することで、マウントのたびに再フェッチされるのを防ぐ- データフェッチのみが目的なら TanStack Query(
useQuery)の方がシンプルに書ける