Day 6: RTK Query応用
今日学ぶこと
- タグによるキャッシュ無効化(
providesTags、invalidatesTags) - タグタイプとタグIDによる精密な無効化
onQueryStartedによる楽観的更新- 悲観的更新パターン
pollingIntervalによるポーリングusePrefetchによるプリフェッチ- ページネーションパターン
transformResponseによるレスポンス変換injectEndpointsによるコード分割
タグによるキャッシュ無効化
Day 5で学んだRTK Queryの自動キャッシュには一つ課題があります。データを変更(作成・更新・削除)した後、キャッシュされた一覧データが古いままになることです。タグシステムがこの問題を解決します。
sequenceDiagram
participant UI as UIコンポーネント
participant Cache as RTK Query Cache
participant API as APIサーバー
UI->>Cache: useGetPostsQuery()
Cache->>API: GET /posts
API-->>Cache: posts data
Cache-->>UI: データ返却
Note over Cache: タグ: ['Post'] を付与
UI->>Cache: useCreatePostMutation()
Cache->>API: POST /posts
API-->>Cache: 新しいpost
Note over Cache: タグ 'Post' を無効化
Cache->>API: GET /posts (自動再取得)
API-->>Cache: 更新されたposts data
Cache-->>UI: 最新データ返却
基本的なタグ設定
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),
// 使用するタグタイプを宣言
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
// このクエリが提供するタグ
providesTags: ['Post'],
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`,
// IDごとに個別のタグを提供
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
// このミューテーションが無効化するタグ
invalidatesTags: ['Post'],
}),
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: patch,
}),
// 特定のIDのタグだけを無効化
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
}),
}),
});
TypeScript版
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts',
providesTags: ['Post'],
}),
getPostById: builder.query<Post, number>({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
createPost: builder.mutation<Post, Omit<Post, 'id'>>({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
invalidatesTags: ['Post'],
}),
updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: patch,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
deletePost: builder.mutation<void, number>({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
}),
}),
});
タグIDによる精密な無効化
タグにIDを付与することで、一覧全体ではなく特定の項目だけを再取得できます。
flowchart TB
subgraph Tags["タグの構造"]
T1["{ type: 'Post', id: 'LIST' }"]
T2["{ type: 'Post', id: 1 }"]
T3["{ type: 'Post', id: 2 }"]
T4["{ type: 'Post', id: 3 }"]
end
subgraph Invalidation["無効化パターン"]
I1["'Post' を無効化<br/>→ すべてのPostタグが無効"]
I2["{ type: 'Post', id: 1 } を無効化<br/>→ ID:1のみ再取得"]
I3["{ type: 'Post', id: 'LIST' } を無効化<br/>→ 一覧のみ再取得"]
end
style Tags fill:#3b82f6,color:#fff
style Invalidation fill:#f59e0b,color:#fff
高度なタグパターン
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
// 各投稿のIDと一覧全体のタグを提供
providesTags: (result) =>
result
? [
// 各投稿に個別のタグ
...result.map(({ id }) => ({ type: 'Post', id })),
// 一覧全体のタグ
{ type: 'Post', id: 'LIST' },
]
: [{ type: 'Post', id: 'LIST' }],
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
// 新規作成時は一覧を再取得(個別の投稿は不要)
invalidatesTags: [{ type: 'Post', id: 'LIST' }],
}),
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: patch,
}),
// 更新時は該当IDのみ再取得
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
// 削除時は一覧と該当IDを無効化
invalidatesTags: (result, error, id) => [
{ type: 'Post', id },
{ type: 'Post', id: 'LIST' },
],
}),
}),
TypeScript版
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post' as const, id })),
{ type: 'Post' as const, id: 'LIST' },
]
: [{ type: 'Post' as const, id: 'LIST' }],
}),
createPost: builder.mutation<Post, Omit<Post, 'id'>>({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
invalidatesTags: [{ type: 'Post', id: 'LIST' }],
}),
updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: patch,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
deletePost: builder.mutation<void, number>({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
invalidatesTags: (result, error, id) => [
{ type: 'Post', id },
{ type: 'Post', id: 'LIST' },
],
}),
}),
複数のタグタイプ
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post', 'Comment', 'User'],
endpoints: (builder) => ({
getPostWithComments: builder.query({
query: (postId) => `/posts/${postId}?_embed=comments`,
// 複数のタグタイプを提供
providesTags: (result, error, postId) => [
{ type: 'Post', id: postId },
{ type: 'Comment', id: `POST_${postId}` },
],
}),
addComment: builder.mutation({
query: ({ postId, ...comment }) => ({
url: '/comments',
method: 'POST',
body: { postId, ...comment },
}),
// コメント追加時に関連する投稿のコメントも無効化
invalidatesTags: (result, error, { postId }) => [
{ type: 'Comment', id: `POST_${postId}` },
],
}),
}),
});
楽観的更新(Optimistic Updates)
楽観的更新は、サーバーの応答を待たずにUIを即座に更新するテクニックです。ユーザー体験が大幅に向上します。
sequenceDiagram
participant UI as UI
participant Cache as Cache
participant API as API
UI->>Cache: Todo完了をトグル
Cache->>UI: 即座にUIを更新(楽観的)
Cache->>API: PUT /todos/1
alt 成功
API-->>Cache: 200 OK
Note over Cache: キャッシュはすでに最新
else 失敗
API-->>Cache: エラー
Cache->>UI: 元の状態に戻す(ロールバック)
end
楽観的更新の実装
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Todo'],
endpoints: (builder) => ({
getTodos: builder.query({
query: () => '/todos',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Todo', id })),
{ type: 'Todo', id: 'LIST' },
]
: [{ type: 'Todo', id: 'LIST' }],
}),
toggleTodo: builder.mutation({
query: ({ id, completed }) => ({
url: `/todos/${id}`,
method: 'PATCH',
body: { completed },
}),
// 楽観的更新
async onQueryStarted({ id, completed }, { dispatch, queryFulfilled }) {
// 1. キャッシュを即座に更新
const patchResult = dispatch(
api.util.updateQueryData('getTodos', undefined, (draft) => {
const todo = draft.find((t) => t.id === id);
if (todo) {
todo.completed = completed;
}
})
);
try {
// 2. サーバーの応答を待つ
await queryFulfilled;
} catch {
// 3. エラー時はロールバック
patchResult.undo();
}
},
}),
}),
});
TypeScript版
interface Todo {
id: number;
title: string;
completed: boolean;
userId: number;
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Todo'],
endpoints: (builder) => ({
getTodos: builder.query<Todo[], void>({
query: () => '/todos',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Todo' as const, id })),
{ type: 'Todo' as const, id: 'LIST' },
]
: [{ type: 'Todo' as const, id: 'LIST' }],
}),
toggleTodo: builder.mutation<Todo, { id: number; completed: boolean }>({
query: ({ id, completed }) => ({
url: `/todos/${id}`,
method: 'PATCH',
body: { completed },
}),
async onQueryStarted({ id, completed }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getTodos', undefined, (draft) => {
const todo = draft.find((t) => t.id === id);
if (todo) {
todo.completed = completed;
}
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
}),
}),
});
楽観的更新を使うTodoコンポーネント
function TodoList() {
const { data: todos } = useGetTodosQuery();
const [toggleTodo] = useToggleTodoMutation();
return (
<ul>
{todos?.map((todo) => (
<li
key={todo.id}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer',
}}
onClick={() =>
toggleTodo({ id: todo.id, completed: !todo.completed })
}
>
{todo.title}
</li>
))}
</ul>
);
}
悲観的更新
悲観的更新は、サーバーの応答を受け取ってからキャッシュを更新するパターンです。データの整合性が重要な場合に使います。
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'],
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
// 悲観的更新: サーバー応答後にキャッシュを更新
async onQueryStarted(newPost, { dispatch, queryFulfilled }) {
try {
const { data: createdPost } = await queryFulfilled;
// サーバーから返されたデータでキャッシュを更新
dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
draft.push(createdPost);
})
);
} catch {
// エラー時は何もしない(キャッシュは変更されていない)
}
},
}),
}),
});
TypeScript版
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts',
providesTags: ['Post'],
}),
createPost: builder.mutation<Post, Omit<Post, 'id'>>({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
async onQueryStarted(newPost, { dispatch, queryFulfilled }) {
try {
const { data: createdPost } = await queryFulfilled;
dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
draft.push(createdPost);
})
);
} catch {
// No rollback needed
}
},
}),
}),
});
楽観的 vs 悲観的更新の使い分け
| 方式 | UI応答速度 | データ整合性 | 適した場面 |
|---|---|---|---|
| 楽観的更新 | 高速(即座) | リスクあり | いいね、トグル、並べ替え |
| 悲観的更新 | 遅い(応答待ち) | 安全 | 決済、重要データの作成 |
| タグ無効化 | 遅い(再取得) | 安全 | 汎用的なCRUD |
ポーリング
pollingIntervalを使うと、一定間隔でデータを自動再取得できます。リアルタイムダッシュボードなどに有用です。
function LiveDashboard() {
// 30秒ごとにデータを自動更新
const { data: stats, isLoading } = useGetDashboardStatsQuery(undefined, {
pollingInterval: 30000, // 30秒
});
if (isLoading) return <div>読み込み中...</div>;
return (
<div>
<h2>ダッシュボード</h2>
<div>アクティブユーザー: {stats?.activeUsers}</div>
<div>本日の売上: ¥{stats?.todaySales?.toLocaleString()}</div>
<div>注文数: {stats?.orderCount}</div>
</div>
);
}
ポーリングの制御
function PollingControl() {
const [pollingInterval, setPollingInterval] = useState(0);
const { data } = useGetNotificationsQuery(undefined, {
// 0を渡すとポーリング停止
pollingInterval,
// ウィンドウがフォーカスされていない時はスキップ
skipPollingIfUnfocused: true,
});
return (
<div>
<button onClick={() => setPollingInterval(5000)}>5秒間隔で更新</button>
<button onClick={() => setPollingInterval(30000)}>30秒間隔で更新</button>
<button onClick={() => setPollingInterval(0)}>ポーリング停止</button>
</div>
);
}
リアルタイムダッシュボードの完全な例
// features/api/dashboardApi.js
const dashboardApi = createApi({
reducerPath: 'dashboardApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Stats'],
endpoints: (builder) => ({
getDashboardStats: builder.query({
query: () => '/dashboard/stats',
providesTags: ['Stats'],
}),
getRecentOrders: builder.query({
query: () => '/dashboard/orders/recent',
providesTags: ['Stats'],
}),
getAlerts: builder.query({
query: () => '/dashboard/alerts',
}),
}),
});
export const {
useGetDashboardStatsQuery,
useGetRecentOrdersQuery,
useGetAlertsQuery,
} = dashboardApi;
// Dashboard.jsx
function Dashboard() {
const { data: stats } = useGetDashboardStatsQuery(undefined, {
pollingInterval: 30000,
});
const { data: orders } = useGetRecentOrdersQuery(undefined, {
pollingInterval: 10000,
});
const { data: alerts } = useGetAlertsQuery(undefined, {
pollingInterval: 5000,
});
return (
<div>
<StatsCards stats={stats} />
<OrderTable orders={orders} />
<AlertBanner alerts={alerts} />
</div>
);
}
プリフェッチ
usePrefetchを使うと、ユーザーが操作する前にデータを事前に取得できます。
function PostList() {
const { data: posts } = useGetPostsQuery();
// プリフェッチ関数を取得
const prefetchPost = usePrefetch('getPostById');
return (
<ul>
{posts?.map((post) => (
<li
key={post.id}
// マウスホバー時にプリフェッチ
onMouseEnter={() => prefetchPost(post.id)}
>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
プリフェッチのオプション
function PostList() {
const prefetchPost = usePrefetch('getPostById', {
// データが60秒以上古い場合のみプリフェッチ
ifOlderThan: 60,
});
// または force オプションで常にプリフェッチ
const prefetchPostForce = usePrefetch('getPostById', {
force: true,
});
return (
<ul>
{posts?.map((post) => (
<li
key={post.id}
onMouseEnter={() => prefetchPost(post.id)}
onFocus={() => prefetchPost(post.id)}
>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
ページネーション
RTK Queryでページネーションを実装するパターンを紹介します。
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: ({ page = 1, limit = 10 }) =>
`/posts?_page=${page}&_limit=${limit}`,
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post', id })),
{ type: 'Post', id: 'PARTIAL-LIST' },
]
: [{ type: 'Post', id: 'PARTIAL-LIST' }],
}),
}),
});
ページネーションコンポーネント
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data: posts, isLoading, isFetching } = useGetPostsQuery({
page,
limit: 10,
});
const prefetchPosts = usePrefetch('getPosts');
return (
<div>
{isLoading ? (
<div>読み込み中...</div>
) : (
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
{posts?.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
)}
<div>
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
>
前のページ
</button>
<span>ページ {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
// 次のページをプリフェッチ
onMouseEnter={() =>
prefetchPosts({ page: page + 1, limit: 10 })
}
>
次のページ
</button>
</div>
</div>
);
}
transformResponse
サーバーのレスポンスを変換してからキャッシュに保存できます。
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
// レスポンスから必要なデータだけ抽出
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) =>
response.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
})),
}),
// ネストされたデータを平坦化
getPostComments: builder.query({
query: (postId) => `/posts/${postId}/comments`,
transformResponse: (response) =>
response.map((comment) => ({
...comment,
shortBody: comment.body.substring(0, 100),
})),
}),
// ソートや正規化
getSortedPosts: builder.query({
query: () => '/posts',
transformResponse: (response) =>
[...response].sort((a, b) =>
a.title.localeCompare(b.title)
),
}),
}),
});
TypeScript版
interface UserRaw {
id: number;
name: string;
email: string;
phone: string;
website: string;
company: { name: string };
address: { city: string };
}
interface UserSimple {
id: number;
name: string;
email: string;
}
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUsers: builder.query<UserSimple[], void>({
query: () => '/users',
transformResponse: (response: UserRaw[]) =>
response.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
})),
}),
}),
});
カスタムbaseQuery
認証トークンの自動付与やエラーハンドリングの共通化にカスタムbaseQueryを使います。
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// トークンリフレッシュ付きのbaseQuery
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.accessToken;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
});
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
if (result?.error?.status === 401) {
// トークンをリフレッシュ
const refreshResult = await baseQuery(
{ url: '/auth/refresh', method: 'POST' },
api,
extraOptions
);
if (refreshResult?.data) {
// 新しいトークンを保存
api.dispatch(setCredentials(refreshResult.data));
// 元のリクエストを再試行
result = await baseQuery(args, api, extraOptions);
} else {
// リフレッシュ失敗 → ログアウト
api.dispatch(logout());
}
}
return result;
};
export const api = createApi({
baseQuery: baseQueryWithReauth,
tagTypes: ['Post', 'User'],
endpoints: () => ({}),
});
TypeScript版
import {
fetchBaseQuery,
BaseQueryFn,
FetchArgs,
FetchBaseQueryError,
} from '@reduxjs/toolkit/query/react';
import { RootState } from '../../store';
import { setCredentials, logout } from '../auth/authSlice';
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.accessToken;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
});
const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);
if (result?.error?.status === 401) {
const refreshResult = await baseQuery(
{ url: '/auth/refresh', method: 'POST' },
api,
extraOptions
);
if (refreshResult?.data) {
api.dispatch(setCredentials(refreshResult.data as { accessToken: string }));
result = await baseQuery(args, api, extraOptions);
} else {
api.dispatch(logout());
}
}
return result;
};
export const api = createApi({
baseQuery: baseQueryWithReauth,
tagTypes: ['Post', 'User'],
endpoints: () => ({}),
});
injectEndpointsによるコード分割
大規模なアプリケーションでは、すべてのエンドポイントを1つのファイルに定義するのは実用的ではありません。injectEndpointsを使ってエンドポイントを分割できます。
// features/api/baseApi.js - 共通のAPI定義
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post', 'User', 'Comment'],
endpoints: () => ({}),
});
// features/api/postsApi.js - 投稿エンドポイント
import { baseApi } from './baseApi';
export const postsApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'],
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
invalidatesTags: ['Post'],
}),
}),
});
export const { useGetPostsQuery, useCreatePostMutation } = postsApi;
// features/api/usersApi.js - ユーザーエンドポイント
import { baseApi } from './baseApi';
export const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['User'],
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
}),
});
export const { useGetUsersQuery, useGetUserByIdQuery } = usersApi;
ストアの設定はbaseApiだけ
// store.js
import { configureStore } from '@reduxjs/toolkit';
import { baseApi } from './features/api/baseApi';
export const store = configureStore({
reducer: {
[baseApi.reducerPath]: baseApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
});
まとめ
今日はRTK Queryの応用テクニックを学びました。
| 概念 | 説明 |
|---|---|
providesTags |
クエリが提供するキャッシュタグ |
invalidatesTags |
ミューテーションが無効化するタグ |
| タグID | 精密なキャッシュ無効化のためのID |
| 楽観的更新 | サーバー応答前にUIを即座に更新 |
| 悲観的更新 | サーバー応答後にキャッシュを更新 |
pollingInterval |
定期的な自動データ再取得 |
usePrefetch |
データの事前取得 |
transformResponse |
レスポンスの変換処理 |
カスタムbaseQuery |
認証やエラーの共通処理 |
injectEndpoints |
エンドポイントのコード分割 |
練習問題
-
基本問題: 投稿とコメントの2つのタグタイプを使い、コメントを追加したときに該当する投稿のコメント一覧だけが再取得されるAPIスライスを作成してください。
-
応用問題: Todoアプリで楽観的更新を実装してください。Todoのタイトル編集時にUIを即座に更新し、サーバーエラー時にはロールバックするようにしましょう。
-
チャレンジ問題: 認証トークンの自動リフレッシュを行うカスタム
baseQueryを実装してください。401エラー時にリフレッシュトークンで新しいアクセストークンを取得し、元のリクエストを再試行する仕組みを作りましょう。injectEndpointsを使ってエンドポイントをファイル分割することも試してみてください。