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>
>;
これで最終的なファイルの構成は以下の通り。
ホント使い勝手が良くなってますね。比較的小さい規模でも導入しやすいと思いました。
コメント