React + Redux Toolkit で REST API を使う

React Redux で面倒に感じる部分が、かなり良い感じに改善しているらしい Redux Toolkit
非同期の部分も加わっている様なので、REST API を試してみました。

基本はテンプレートを使用

Redux Toolkit のイントロダクションでも説明されている通り、次のコマンドでテンプレートを用意しました。

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

REST API は「国土交通省 総合情報システム」が公開している「都道府県内市区町村一覧取得API」で試しました。

テンプレートを参考に REST API を使用するコードを追加

テンプレートが良い感じに参考なります。
なので、同じ構成でコードを追加していきました。

まずは counterAPI.ts を参考に次のコードを追加。

export type city = {
  status: string;
  data: {
    id: string,
    name: string,
  }[];
};

export function fetchCity(code: string) {
  return new Promise<city>((resolve, reject) => {
    fetch(`https://www.land.mlit.go.jp/webland/api/CitySearch?area=${code}`)
      .then(response => resolve(response.json()))
      .then(data => reject(data));
  });
}

REST API を呼んで非同期に実行させる部分になります。

次に counterSlice.ts を参考に次のコードを追加。

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { RootState } from '../../app/store';
import { 
  city,
  fetchCity,
} from './cityAPI';

export interface CityState {
  data?: city;
  status: 'idle' | 'loading' | 'failed';
}

const initialState: CityState = {
  data: undefined,
  status: 'idle',
};

export const cityAsync = createAsyncThunk(
  'city/fetch',
  async (code: string) => {
    try {
      const response = await fetchCity(code);
      return {
        status: 'idle',
        data: response.data,
      };
    }
    catch (e) {
      return {
        status: 'failed',
        data: [],
      };
    }
  },
);

export const citySlice = createSlice({
  name: 'city',
  initialState,
  reducers: {
    clear: (state) => {
      state.data = { status: 'idle', data: [] };
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(cityAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(cityAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.data = action.payload;
      });
  },
});

export const { clear } = citySlice.actions;
export const selectCity = (state: RootState) => state.city.data;
export default citySlice.reducer;

createAsyncThunk 関数で非同期のアクションを生成し、extraReducers で非同期の結果を反映する感じですね。
cityAsync.rejected に対する処理は省略しましたが。

そして表示部分となる Counter.tsx を参考に次のコードを追加。

import React, { useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import {
  clear,
  cityAsync,
  selectCity,
} from './citySlice';

export function City() {
  const [code, setCode] = useState('13');
  const cities = useAppSelector(selectCity);
  const dispatch = useAppDispatch();
  return (
    <div>
      <input type="text" value={code} onChange={(e) => setCode(e.target.value)} />
      <button onClick={() => dispatch(cityAsync(code))}>get</button>
      <button onClick={() => dispatch(clear())}>clear</button>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>name</th>
          </tr>
        </thead>
        <tbody>
          {
            cities?.data?.map(c => 
              <tr key={c.id}>
                <td>{c.id}</td>
                <td>{c.name}</td>
              </tr>
              )
          }
        </tbody>
      </table>
    </div>
  );
}

実行時には次のような形になります。

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

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import cityReducer from '../features/city/citySlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    city: cityReducer,
  },
});

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

これで最終的なファイルの構成は以下の通り。

ホント使い勝手が良くなってますね。比較的小さい規模でも導入しやすいと思いました。

コメント