RTK Queryを始めてみてナニコレイミワカラナイから便利やなーぐらいになった人の
概要
仕事で開発をしている時、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のベーシックなやつです!
# ✅ 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"]
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つに分けて実装していきます
- Redux Toolkit のセットアップ
- API Slice の作成
- Provider で Redux ストアを適用
- コンポーネントでデータ取得
Step1 Redux Toolkit のセットアップ
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の指定を省略できます
- fetchをラッパーした関数です。と言われて私は分からなかったので私なりに咀嚼するとAPIを呼び出すときに設定する共通の値といったイメージです
- endpoints
- 実際にコールするAPIのエンドポイントを設定します。ここでは、getUsersというuserを取得するGETの処理、addUserというユーザを新規追加するPOSTの処理を追加しています
GETメソッドを作成する場合はbuilder.query
をGET以外のHTTPメソッドを作成する場合は、builder.mutation
を使用します。
- 実際にコールするAPIのエンドポイントを設定します。ここでは、getUsersというuserを取得するGETの処理、addUserというユーザを新規追加するPOSTの処理を追加しています
- use〇〇Query
- endpointsに記載したキーでRTK Query側がよしなに作ってくれるhooks。GETメソッドの場合作成される。実際にAPIを実行する際はexportしたuseGetUsersQueryを使用して実行する
- use〇〇Mutation
- GETメソッド以外のHTTPメソッドの場合作成される
Step3 Provider で Redux ストアを適用
main.tsxにProviderを追加します。
Providerが何をしているかというと、定義したStoreをProviderで囲ったコンポーネントで使うよ!
といった感じで、下記の実装だとアプリ全体でStoreを使うイメージです。
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を実行
する画面を用意します。
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を実行したら指定したタグのデータを再取得させます
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行で実装することができます
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