🧏

RTK Queryを始めてみてナニコレイミワカラナイから便利やなーぐらいになった人の

2025/02/24に公開

概要

仕事で開発をしている時、RTK Queryなるものに出会いました
そもそもReduxが得意ではない点 + fetchで頑張るおじさんだった私が始めて出会った時
ナニコレイミワカラナイ・・・と一回PCを閉じたのですが、ちょっと基礎を触ってみて
便利やなーとなる過程をただつらつら書いただけの記事です🙌

RTK Qeuryってそもそも何よ

RTK Queryと言うのはRedux Toolkitに含まれているデータフェッチ・キャッシュ管理を良しなーにやってくれる便利ツールです。
fetchやaxiosを使って手動で状態管理する代わりに、RTK Queryを使えば非同期処理をReduxのストアでスマートに扱えます。
useEffect + fetch + useState でやってた処理が、RTK Queryならよりシンプルなコードで実現できるという便利ーなツールです✌️

この記事で伝えたいこと

  • fetchが簡単に書ける!
  • キャッシュしてくれる!
  • 対象データが更新された場合の再取得やポーリングなどAPIでオプショナルにつけたい処理が簡単に書ける!

上記の3点になります!

早速実装してみる

それでは早速実際に手を動かしてみて確認していきます!

環境構築

まずは、環境構築なのですが・・・今回の趣旨とは違うのでここはすっ飛ばします!(書くの大変だから・・・とかではありません?🫠)

構成だけ描くと、vite + react + typescriptのベーシックなやつです!

Dockerfile
# ✅ Node.jsの最新版を使用
FROM node:18-alpine

# ✅ 作業ディレクトリを設定
WORKDIR /app

# ✅ Viteの開発サーバー用のポート
EXPOSE 5173

# ✅ 依存関係をインストールするための前準備
COPY client/package.json client/package-lock.json ./

# ✅ 依存関係をインストール
RUN npm install

# ✅ ソースコードをコンテナ内にコピー
COPY client/ .

# ✅ Vite 開発サーバーを起動
CMD ["npm", "run", "dev"]
docker-compose
version: "3.9"
services:
  app:
    container_name: app
    build: .
    ports:
      - "5173:5173" # Viteの開発サーバー
    volumes:
      - ./client:/app # ソースコードを同期
      - /app/node_modules # node_modulesをホストと分離
    command: /bin/sh -c "npm run dev" # 開発サーバーを起動

肝であるfetchの処理を書いてみる!

大きくStepを4つに分けて実装していきます

  1. Redux Toolkit のセットアップ
  2. API Slice の作成
  3. Provider で Redux ストアを適用
  4. コンポーネントでデータ取得

Step1 Redux Toolkit のセットアップ

store.tsファイルを作成し、下記のようなコードを記載します。

store.ts
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import { api } from "./services/api"; // 🔥 後で作成するAPIサービス

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer, // 🔹 APIサービスのリデューサーを登録
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware), // 🔹 APIのミドルウェアを追加
});

// 🔥 RTK Query のキャッシュリスナーをセットアップ(再フェッチとかの制御用)
setupListeners(store.dispatch);

// ストアの型を定義
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

まず、configureStore関数を使ってStoreを作成します
Reduxを使い慣れている方からしたらいつものだとは思うのですが、データを保存しておく場所といったイメージのものを定義します。
[api.reducerPath]: api.reducerはどのApiがどのデータを持っているかのような紐付けを行っています、後述のStepで実際にセットするコードがあるのですがイメージとしては下記の通りです。

// ◼︎前提
// usersというユーザ情報を取得するAPIが存在した時、

api.reducerPath = 'users'
api.reducer = 実際にAPIで取得したデータ・ローディング中かどうかの判定フラグ・エラーオブジェクト(なければnull

Step2 API Slice の作成

services/api.tsを作成し、下記のようなコードを記載します。

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

// 🔥 RTK Query の API 定義
export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com" }),
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => "/users",
      providesTags: ["Users"],
    }),
    addUser: builder.mutation<User, Partial<User>>({
      query: (user) => ({
        url: "/users",
        method: "POST",
        body: user,
      }),
      invalidatesTags: ["Users"],
    }),
  }),
});

// フックをエクスポート(🔥 コンポーネントで使う)
export const { useGetUsersQuery, useAddUserMutation } = api;

// API の型定義
export interface User {
  id: number;
  name: string;
  email: string;
}

次に、API Sliceを作成します
まず、Sliceってなんだ?で私はつまづいたので概要説明すると
Step1で作成したStoreを全体のホールケーキと捉えた時の個々のデータ(カットケーキ)を定義します。若干わかりづらいのですごく咀嚼すると、セーブデータを作るイメージです。

重要な各要素の説明をします

  • createApi
    • 名前の通りAPIを作成するときに使うものです、一旦やんわりこの理解でOKかなと🫠
  • reducerPath
    • セーブデータの名前を決めることができます、ここではusersAPIから取得したデータを保存しておく場所という意味合いでusersと名をつけています。
  • baseQuery
    • fetchをラッパーした関数です。と言われて私は分からなかったので私なりに咀嚼するとAPIを呼び出すときに設定する共通の値といったイメージです
      ここではリクエスト先のURLを共通の設定としています、このように設定することでpost・put・patchなどのAPIを追加した際に大元のpathの指定を省略できます
  • endpoints
    • 実際にコールするAPIのエンドポイントを設定します。ここでは、getUsersというuserを取得するGETの処理、addUserというユーザを新規追加するPOSTの処理を追加しています
      GETメソッドを作成する場合はbuilder.queryをGET以外のHTTPメソッドを作成する場合は、builder.mutationを使用します。
  • use〇〇Query
    • endpointsに記載したキーでRTK Query側がよしなに作ってくれるhooks。GETメソッドの場合作成される。実際にAPIを実行する際はexportしたuseGetUsersQueryを使用して実行する
  • use〇〇Mutation
    • GETメソッド以外のHTTPメソッドの場合作成される

Step3 Provider で Redux ストアを適用

main.tsxにProviderを追加します。
Providerが何をしているかというと、定義したStoreをProviderで囲ったコンポーネントで使うよ!
といった感じで、下記の実装だとアプリ全体でStoreを使うイメージです。

main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Step4 コンポーネントでデータ取得

実際に、コンポーネントにStep1~3で用意してきたAPIを実装し、動作を確認します
ここでは簡潔に、

  • 画面レンダリング時にGETメソッドのusersAPIを実行
  • ボタン押下時にPOSTメソッドのusersAPIを実行
    する画面を用意します。
App.tsx
import { useGetUsersQuery, useAddUserMutation } from "./services/api.ts";
import type { User } from "./services/api.ts";

function App() {
  // 🔥 RTK Query のフックを実行
  const { data: users, error, isLoading } = useGetUsersQuery(undefined, { pollingInterval: 5000 });
  const [addUser] = useAddUserMutation();
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error fetching users.</p>;

  return (
    <div>
      <button onClick={async () => {
        await addUser({ name: "John Doe", email: "john@example.com" });
      }}>Add User</button>
      <h1>Users List</h1>
      <ul>
        {users?.map((user: User) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

ここで、注目したいのが、useEffectなどを使わないにも関わらず初期表示時にGETのuserAPIが呼び出せているということです。
これは、useGetUsersQueryを実行した際によしなに初期レンダリング時にAPIをコールしてくれています便利ですね🙌呼び出されたくない場合はちゃんとスキップする方法もあるのでご安心を!

実際に画面をみてみる

おー実際に、GET UsersAPIが呼ばれ画面にユーザのリストが表示されていますね!
次にAdd Userボタンを押してみます

POSTリクエストも無事飛びました🎉
ここで一番下のリクエストを見てください。POSTのリクエストを飛ばしただけなのにもう1個userのリクエストが飛んでいますよね?これはGETメソッドが呼ばれています。
RTK Queryでは、データが更新された際に該当するAPIの再fetchを簡単に実装することができます。

というのも、もうすでにここまでに実装は済んでいて
api.tsのprovidesTags要素とinvalidatesTags要素に注目してください

  • providesTags
    • タグの定義このAPIはこういう者ですという証明
  • invalidatesTags
    • このAPIを実行したら指定したタグのデータを再取得させます
      といった要素になっており、この2行を追加するだけで再fetchを実装できます、便利ですねー😊
api.ts
export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com" }),
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => "/users",
      providesTags: ["Users"],
    }),
    addUser: builder.mutation<User, Partial<User>>({
      query: (user) => ({
        url: "/users",
        method: "POST",
        body: user,
      }),
      invalidatesTags: ["Users"],
    }),
  }),
});

と、説明している間に画面を放置しているとリクエストが勝手に飛んでいました

これはバグか!?安心してください、これも実はすでに実装していていわゆるポーリングというやつです。ポーリングといえば定期的にデータを取ってきてねーのやつですね。
実はこれもRTK Queryであればわずか1行で実装することができます

App.tsx
import { useGetUsersQuery, useAddUserMutation } from "./services/api.ts";
import type { User } from "./services/api.ts";

function App() {
  // 🔥 RTK Query のフックを実行
  const { data: users, error, isLoading } = useGetUsersQuery(undefined, { pollingInterval: 5000 });
  const [addUser] = useAddUserMutation();
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error fetching users.</p>;

  return (
    <div>
      <button onClick={async () => {
        await addUser({ name: "John Doe", email: "john@example.com" });
      }}>Add User</button>
      <h1>Users List</h1>
      <ul>
        {users?.map((user: User) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

上記のコードの
const { data: users, error, isLoading } = useGetUsersQuery(undefined, { pollingInterval: 5000 });
の箇所です。
RTK Queryによって発行されたhooksである、useGetUsersQueryの第二引数には様々なオプションが設定でき、今回はpollingIntervalというポーリングを実行する設定値を持たせています。
わずかこれだけでポーリングの実装が終わりました、便利ですねー
(ちなみに第一引数はAPIに渡すリクエストの引数です、今回は特に引数指定なしのAPIなのでundefinedになっています🙏)

まとめ

最初見た時はナニコレイミワカラナイ、ワタシニハツカエナイといった感じだったのですが
ほーん便利そうだなー使ってみたいなーには自分の中で持っていけたと思っています。
RTK Query以外にも同様なライブラリは存在しますが、Reduxとの相性は最高だと思うので使ったことない方いらっしゃいましたらぜひこの機会に!😎

Discussion