レビュー待ち·難易度: 中級·更新: 2026-04-18

Redux Toolkit の RTK Query で API データフェッチとキャッシュを管理する

RTK Query の createApi でエンドポイントを定義し、useGetQuery / useMutation フックでデータフェッチ・キャッシュ・自動再フェッチを管理する例。

nextjsapistate-managementredux-toolkit

対応バージョン

nextjs 15react 19redux-toolkit 2

前提環境

Redux Toolkit の createSlice と React-Redux の useSelector / useDispatch を理解していること

概要

createApi でエンドポイントを定義すると、RTK Query がデータフェッチ・キャッシュ・ローディング状態・自動再フェッチを自動で管理するフックを生成する。createAsyncThunk による手動キャッシュ管理が不要になり、Redux Store 内で TanStack Query 相当の機能を実現できる。

インストール

npm install @reduxjs/toolkit react-redux

実装

API スライスの定義

// store/api/usersApi.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

type User = { id: number; name: string; email: string };
type CreateUserInput = { name: string; email: string };

export const usersApi = createApi({
  reducerPath: "usersApi",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  // キャッシュの無効化タグ
  tagTypes: ["User"],
  endpoints: (builder) => ({
    // GET /api/users → useGetUsersQuery
    getUsers: builder.query<User[], void>({
      query: () => "/users",
      providesTags: ["User"],
    }),

    // GET /api/users/:id → useGetUserByIdQuery
    getUserById: builder.query<User, number>({
      query: (id) => `/users/${id}`,
      providesTags: (_result, _error, id) => [{ type: "User", id }],
    }),

    // POST /api/users → useCreateUserMutation
    createUser: builder.mutation<User, CreateUserInput>({
      query: (body) => ({
        url: "/users",
        method: "POST",
        body,
      }),
      // 成功時に User タグを持つキャッシュを無効化 → 一覧が自動再フェッチされる
      invalidatesTags: ["User"],
    }),

    // DELETE /api/users/:id → useDeleteUserMutation
    deleteUser: builder.mutation<void, number>({
      query: (id) => ({
        url: `/users/${id}`,
        method: "DELETE",
      }),
      invalidatesTags: ["User"],
    }),
  }),
});

export const {
  useGetUsersQuery,
  useGetUserByIdQuery,
  useCreateUserMutation,
  useDeleteUserMutation,
} = usersApi;

Store への組み込み

// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { usersApi } from "./api/usersApi";

export const store = configureStore({
  reducer: {
    [usersApi.reducerPath]: usersApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(usersApi.middleware),
});

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

Provider の設定

// app/providers.tsx
"use client";

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

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}

Query フック(一覧取得)

// components/UserList.tsx
"use client";

import { useGetUsersQuery, useDeleteUserMutation } from "@/store/api/usersApi";

export function UserList() {
  const { data: users, isLoading, isError } = useGetUsersQuery();
  const [deleteUser] = useDeleteUserMutation();

  if (isLoading) return <p className="text-sm text-gray-500">読み込み中...</p>;
  if (isError) return <p className="text-sm text-red-500">エラーが発生しました</p>;

  return (
    <ul className="space-y-2">
      {users?.map((user) => (
        <li key={user.id} className="flex items-center justify-between rounded border p-3">
          <div>
            <p className="font-medium text-gray-800">{user.name}</p>
            <p className="text-xs text-gray-500">{user.email}</p>
          </div>
          <button
            onClick={() => deleteUser(user.id)}
            className="rounded bg-red-50 px-3 py-1 text-xs text-red-600 hover:bg-red-100"
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  );
}

Mutation フック(作成)

// components/CreateUserForm.tsx
"use client";

import { useState } from "react";
import { useCreateUserMutation } from "@/store/api/usersApi";

export function CreateUserForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [createUser, { isLoading, isError }] = useCreateUserMutation();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    await createUser({ name, email });
    setName("");
    setEmail("");
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-3">
      <input
        type="text"
        placeholder="名前"
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="block w-full rounded border px-3 py-2 text-sm"
      />
      <input
        type="email"
        placeholder="メール"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="block w-full rounded border px-3 py-2 text-sm"
      />
      <button
        type="submit"
        disabled={isLoading || !name || !email}
        className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
      >
        {isLoading ? "追加中..." : "ユーザーを追加"}
      </button>
      {isError && <p className="text-xs text-red-500">作成に失敗しました</p>}
    </form>
  );
}

ポイント

  • createApiendpoints でエンドポイントを宣言すると、useGetUsersQuery / useCreateUserMutation などのフックが自動生成される。手動で dispatch(fetchUsers()) を書く必要がない
  • providesTags / invalidatesTags でキャッシュの依存関係を宣言すると、Mutation 成功後に関連する Query が自動再フェッチされる(createUser 後に一覧が更新される)
  • redux-toolkit-async-thunk との違い: createAsyncThunk はフェッチ処理と state 更新を手動で書く。RTK Query はエンドポイント定義だけでキャッシュ・ローディング・エラー状態を自動管理する
  • isLoading / isFetching / isError / data が自動で取れるため、ローディング UI の実装が簡潔になる
  • TanStack Query との使い分け: Redux を既に使っていてキャッシュも Redux に統一したい場合は RTK Query、Redux を使わない構成では TanStack Query が軽量

注意点

redux-toolkit-async-thunk は createAsyncThunk で手動キャッシュ管理。RTK Query は createApi でエンドポイント定義 + 自動キャッシュ・無効化を提供する(TanStack Query と同様の機能を Redux 内で実現)。

関連サンプル