npm install @reduxjs/toolkit react-redux
・src/providers/ReduxProvider.tsx
・src/stores/myApi.ts
・src/stores/store.ts
src/providers/ReduxProvider.tsx
'use client';
import React from 'react';
import { store } from '../stores/store';
import { Provider } from 'react-redux';
export const ReduxProvider = (props: React.PropsWithChildren) => {
return <Provider store={store}>{props.children}</Provider>;
};
src/stores/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { myApi } from './myApi';
import { setupListeners } from '@reduxjs/toolkit/query';
export const store = configureStore({
reducer: {
[myApi.reducerPath]: myApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(myApi.middleware),
});
setupListeners(store.dispatch);
src/stores/myApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
type People = {
name: string;
height: string;
mass: string;
};
export const myApi = createApi({
reducerPath: 'myApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://swapi.dev/api/' }),
endpoints: (builder) => ({
getPeople: builder.query<People, string>({
query: (id) => `people/${id}`,
}),
}),
});
export const { useGetPeopleQuery, useLazyGetPeopleQuery } = myApi;
以下の命名でフックが作成されます。
// use + <定義した名称> + Query
// useLazy + <定義した名称> + Query
違いは「呼び出した時に自動でクエリ実行される」or「クエリを即座に実行せず、後で手動で実行するための関数を返す」です。
src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { ReduxProvider } from './providers/ReduxProvider.tsx';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ReduxProvider>
<App />
</ReduxProvider>
</React.StrictMode>
);
src/App.tsx
import './App.css';
import { useGetPeopleQuery } from './stores/myApi';
function App() {
const { data, isLoading } = useGetPeopleQuery('2');
return (
<>
<h1>sample</h1>
{isLoading && <div>Loading...</div>}
<div>{JSON.stringify(data)}</div>
</>
);
}
export default App;
useGetPeopleQuery('2'); を実行した瞬間に API のリクエストが走ります。
const [trigger, {data, isFetching}, lastPromiseInfo] = useLazyGetTimeQuery();
const handleClick = async () => {
trigger()
}
return(
<>
<h4>{isFetching ? 'ローディング中' : JSON.stringify()}</h4>
<button type={'button'} onClick={handleClick} >LazyQuery実行</button>
</>
)
useLazyクエリ の場合はtrigger関数を受け取って使用します。また useLazyGetTimeQuery() 実行時に API へのリクエストは走りません
trigger関数は Promise を返すので以下のように使うこともできます。
const [trigger, {data, isFetching}, lastPromiseInfo] = useLazyGetTimeQuery();
const handleClick = async () => {
try {
const result = await trigger();
console.log("クエリ結果:", result.data);
} catch (error) {
console.error("エラー発生:", error);
}
};
useLazyクエリ の場合はtrigger関数を受け取って使用します。また useLazyGetTimeQuery() 実行時に API へのリクエストは走りません
エラーログを表示するミドルウェアを作成してみます。
src/middlewares/errorLogMiddleware.ts
import {
Middleware,
isRejectedWithValue,
MiddlewareAPI,
} from '@reduxjs/toolkit';
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
type EventData = {
eventCategory: string;
eventAction: string;
eventLabel: string;
};
export const errorLogMiddleware: Middleware =
(_: MiddlewareAPI) => (next) => (action) => {
if (isRejectedWithValue(action)) {
const errorPayload = action.payload as FetchBaseQueryError;
const errorData: string = errorPayload.data as string;
// エラー情報からイベントデータを作成
const eventData: EventData = {
eventCategory: 'API Error',
eventAction: errorPayload.status as string,
eventLabel: errorData,
};
// エラーを表示
console.error('● eventData');
console.error(eventData);
}
return next(action);
};
src/stores/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { myApi } from './myApi';
import { setupListeners } from '@reduxjs/toolkit/query';
import { errorLogMiddleware } from '../middlewares/errorLogMiddleware';
export const store = configureStore({
reducer: {
[myApi.reducerPath]: myApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(myApi.middleware).concat(errorLogMiddleware),
});
setupListeners(store.dispatch);
その他参考 :
https://qiita.com/7tsuno/items/2301a35283db7cd54df9
https://zenn.dev/snamiki1212/scraps/3c7190317c5e8f
キャッシュ制御として
tagTypes: ['Item']
または
.enhanceEndpoints({
addTagTypes: ["Item"],
})
をセットします。そして、クエリの場合は providesTags を、ミューテーションの場合は invalidatesTags をセットします。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Item'],
endpoints: (builder) => ({
getItems: builder.query({
query: () => '/items',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Item', id })),
{ type: 'Item', id: 'LIST' },
]
: [{ type: 'Item', id: 'LIST' }],
}),
createItem: builder.mutation({
query: (newItem) => ({
url: '/items',
method: 'POST',
body: newItem,
}),
invalidatesTags: [{ type: 'Item', id: 'LIST' }],
}),
updateItem: builder.mutation({
query: ({ id, ...updateData }) => ({
url: `/items/${id}`,
method: 'PUT',
body: updateData,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Item', id },
{ type: 'Item', id: 'LIST' },
],
}),
deleteItem: builder.mutation({
query: (id) => ({
url: `/items/${id}`,
method: 'DELETE',
}),
invalidatesTags: (result, error, id) => [
{ type: 'Item', id },
{ type: 'Item', id: 'LIST' },
],
}),
getItemById: builder.query({
query: (id) => `/items/${id}`,
providesTags: (result, error, id) => [{ type: 'Item', id }],
}),
}),
});
export const {
useGetItemsQuery,
useCreateItemMutation,
useUpdateItemMutation,
useDeleteItemMutation,
useGetItemByIdQuery,
} = api;
export default api;
Unlike useQuery, useMutation returns a tuple. The first item in the tuple is the "trigger" function and the second element contains an object with status, error, and data.
useQuery とは異なり、useMutation はタプルを返します。タプルの最初の項目は「トリガー」関数で、2 番目の要素にはステータス、エラー、データを含むオブジェクトが含まれます。
例:
// ミューテーション関数を取得
const [createPost, { isLoading, isSuccess, isError, error }] = useCreatePostMutation();
try {
const result = await createPost(formData).unwrap();
alert('メモが正常に作成されました!');
console.log( result );
} catch (err) {
console.error('メモの作成に失敗しました:', err);
}
mutation 実行時に戻ってくるデータは以下のデータが Promise で返ってきます。
{
data?: MyData;
error?: SerializedError | FetchBaseQueryError;
}
unwrap すると
MyData
が、そのまま取得できるのでぜひ活用しましょう。
const [updatePost, { isLoading: isSaving, isError }] =useUpdatePostMutation();
// 保存する関数
const handleSavePost = async (id: string, data: UpdatePostRequest) => {
try {
const result = await updateMemo({
id: id,
data: data,
}).unwrap();
console.log("Saved ok: ", result);
} catch (error) {
console.error("Failed to save post:", error);
}
};
addPost({ id: 1, name: 'Example' })
.unwrap()
.then((payload) => console.log('fulfilled', payload))
.catch((error) => console.error('rejected', error))
RTK Query では mutation の後に invalidate するタグを渡して invalidate する使い方が基本ですが、 以下のように強制 invalidate できます。
src/stores/myApi.ts
export const myApi = createApi({
reducerPath: 'myApi',
baseQuery: fetchBaseQuery({ baseUrl: '' }),
tagTypes: ['GetTimeTag'],
endpoints: (builder) => ({
getTime: builder.query<WorldTime, void>({
query: () => `https://worldtimeapi.org/api/timezone/Asia/Tokyo`,
keepUnusedDataFor: 4, // キャッシュの保持期間を4秒に設定
providesTags:['GetTimeTag']
}),
}),
});
export const { useGetTimeQuery } = myApi;
export const selectGetTimeResult = myApi.endpoints.getTime.select();
src/components/InvalidateButton.tsx
import { FC } from 'react';
import { myApi, selectGetTimeResult } from '../stores/myApi.ts';
import { useDispatch, useSelector } from 'react-redux';
export const InvalidateButton: FC = () => {
const dispatch = useDispatch();
const cacheDataTimeStamp =
useSelector(selectGetTimeResult)?.fulfilledTimeStamp;
const isCacheValid = (cacheDataTimeStamp: number | undefined): boolean => {
if (cacheDataTimeStamp === undefined) return false;
const currentTime = Date.now();
const cacheDuration = 4 * 1000; // 4秒
return currentTime - cacheDataTimeStamp < cacheDuration;
};
const handleClick = () => {
if (isCacheValid(cacheDataTimeStamp)) {
alert('キャッシュが有効✅です。');
} else {
alert('invalidate❌します。');
dispatch(myApi.util.invalidateTags(['GetTimeTag']));
}
};
return (
<button onClick={handleClick}>
GetTimeTagを持つデータのキャッシュをクリア
</button>
);
};