フロントエンド開発といえば。
react アプリの初期化( npm init vite@latest <アプリ名> )

RTK Query

● RTK Queryのインストール

npm install @reduxjs/toolkit react-redux

● 作成するファイル

・src/providers/ReduxProvider.tsx
・src/stores/myApi.ts
・src/stores/store.ts

● ファイルの作成 1

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>;
};

● ファイルの作成 2

src/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);

● ファイルの作成 3

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 } = myApi;

● 既存ファイルの修正

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>
);

● hooksを使ってデータ取得するサンプル

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;

● RTK Query の Middleware

エラーログを表示するミドルウェアを作成してみます。

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

● RTK Query データを 別のコンポーネントから強制的に invalidate する

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>
  );
};
No.2514
07/07 09:48

edit