Redux Toolkit の RTK Query を定周期に実行する

以前 RTK Query を使ってみましたが、定周期で実行することができるようなので試しました。

テンプレートの作成

create-react-app でテンプレートを作成します。
create-react-app のバージョンを指定しているのはエラーを回避する為です。
v5.0.0が最近(2021/12/14)出たばかりだからかと思います。

npx create-react-app@5.0.0 my-app --template redux-typescript

APIの準備

定期的に取得して結果に違いが出そうな API として、Yahoo! JAPAN Webサービスの気象情報API を利用します。
利用には固有のアプリケーションIDを取得する必要があります。

RTK Query のコードを追加

テンプレートに追加/修正するファイルは以下の4ファイルです。

  1. src/app/services/weather/index.ts
  2. src/app/services/weather/type.ts
  3. src/app/store.ts
  4. src/App.tsx

気象情報APIを実行するコードを以下のように作成しました。
渡した座標(Coordinates)の天気(Weather)を返します。

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { Coordinates, Weather } from './type';

const appId = '(取得した固有のアプリケーションID)';

export const weatherApi = createApi({
  reducerPath: 'weatherApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://map.yahooapis.jp/weather/V1/' }),
  endpoints: (builder) => ({
    getWeather: builder.query<Weather, Coordinates>({
      query: (coordinates: Coordinates) => {
        const {
            latitude,
            longitude,
          } = coordinates;
        return `place?output=json&past=1&coordinates=${longitude},${latitude}&appid=${appId}`;
      } ,
    }),
  }),
});

export const { useGetWeatherQuery } = weatherApi;

Weather の定義は以前紹介した「Paste JSON as Code」を利用し、Coodinates を含め以下のようにしました。

export interface Coordinates {
  latitude: number;
  longitude: number;
}

export interface Weather {
  ResultInfo: ResultInfo;
  Feature:    Feature[];
}

export interface Feature {
  Id:       string;
  Name:     string;
  Geometry: Geometry;
  Property: Property;
}

export interface Geometry {
  Type:        string;
  Coordinates: string;
}

export interface Property {
  WeatherAreaCode: number;
  WeatherList:     WeatherList;
}

export interface WeatherList {
  Weather: Weather[];
}

export interface Weather {
  Type:     string;
  Date:     string;
  Rainfall: number;
}

export interface ResultInfo {
  Count:       number;
  Total:       number;
  Start:       number;
  Status:      number;
  Latency:     number;
  Description: string;
  Copyright:   string;
}

そして、作成した Reducer を既存の store に加えました。

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import { weatherApi } from './services/weather';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    [weatherApi.reducerPath]: weatherApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(weatherApi.middleware),
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

表示部分の作成

既存の App.tsx を以下のように変更しました。

import React, { useState } from 'react';
import { useGetWeatherQuery } from './app/services/weather';

const intervalOptions = [
  { label: 'Off', value: 0 },
  { label: '1分', value: 1 * 60 * 1000 },
  { label: '10分', value: 10 * 60 * 1000 },
  { label: '30分', value: 30 * 60 * 1000 },
];

const Types: { [key: string]: string } = {
  observation: '実測値',
  forecast: '予測値',
};

export function App() {
  const [latitude, setLatitude] = useState(35.663613);
  const [longitude, setLongitude] = useState(139.732293);
  const [pollingInterval, setPollingInterval] = useState(60000);
  const { data, error, isLoading } = useGetWeatherQuery({
    latitude,
    longitude,
  }, { pollingInterval });
  const now = new Date();
  return (
    <>
      <div>
        <label>更新間隔</label>
        <select
          value={pollingInterval}
          onChange={({ target: { value } }) =>
            setPollingInterval(Number(value))
          }
        >
          {intervalOptions.map(({ label, value }) => (
            <option key={value} value={value}>
              {label}
            </option>
          ))}
        </select>
      </div>
      <div>
        <label>経度</label><input type="text" value={longitude} onChange={(e) => setLongitude(Number(e.target.value))} />
      </div>
      <div>
        <label>緯度</label><input type="text" value={latitude} onChange={(e) => setLatitude(Number(e.target.value))} />
      </div>
      {
        error
          ? 'Error.'
          : isLoading
            ? 'Loading...'
            : data && data.Feature
              .map(feature => <>
                <div>{feature.Name}</div>
                <table>
                  <caption>{now.toLocaleDateString()} {now.toLocaleTimeString()} 更新</caption>
                  <thead>
                    <tr>
                      <th>区分</th>
                      <th>日時</th>
                      <th>降水強度(mm/h)</th>
                    </tr>
                  </thead>
                  <tbody>
                    {feature.Property.WeatherList.Weather
                      .map(w =>
                        <tr key={w.Date}>
                          <td>{Types[w.Type]}</td>
                          <td>{w.Date}</td>
                          <td align='right'>{w.Rainfall}</td>
                        </tr>
                      )}
                  </tbody>
                </table>
              </>)
      }
    </>
  );
}

export default App;

API を実行する周期を useGetWeatherQuery のオプションで pollingInterval に指定すれば OK です。
このソースコード自身の注意としては、入力中の座標の天気も取得に行っちゃう作りになっている所ですが妥協しました。

実行結果

「降水強度」が 0 のみだと寂しいので、降りそうな新潟駅辺りを指定してみました。

データの取得と表示も定周期で動きました。
定周期のタイミングで、座標変更前のデータ取得も1度発生しますね。
特に使われることが無いので問題にはならなそうです。

コメント