目的
React Hooks + Redux ToolKit (+ TypeScript + axios)を使った非同期処理の実装を行います。
今回はQiitaのユーザーの記事一覧取得APIを対象にします。
https://qiita.com/api/v2/docs
GET /api/v2/users/:user_id/items
また、Redux Toolkitの詳しい説明はこちらの記事で紹介されているので是非参考にしてみてください。
先に動作イメージを確認する方はこちら
環境構築
今回はRedux + TypeScript のテンプレートを使用します。
npx create-react-app my-app --template redux-typescript
作成したアプリにaxiosを追加します。
axiosをインストール
npm install axios@0.21.1
フォルダ構造は以下に示します(今回使用するフォルダのみ記載しています)。
my-app
└ src
├ App.tsx
├ app
│ └ store.js
└ features
└ qiita
│ Qiita.tsx
└ qiitaSlice.js
実装
実装手順として、qiitaSliceに非同期処理を実装し、そのreducerをstoreに登録します。
その後、storeに登録されているqiitaSliceの状態をQiitaコンポーネントで参照を行う動きになります。
Sliceの実装
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { RootState } from "../../app/store";
interface Item {
rendered_body: string;
body: string;
coediting: boolean;
comments_count: number;
created_at: string;
group: any;
id: string;
likes_count: number;
private: boolean;
reactions_count: number;
tags: {
name: string;
versions: [];
}[];
title: string;
updated_at: string;
url: string;
user: any;
page_views_count: null | number;
team_membership: any;
}
interface Items {
items: Item[];
}
const initialState: Items = {
items: [],
};
/** データ取得非同期処理 */
// 投稿ーデータを取得
export const fetchAsyncGetUserItems = createAsyncThunk(
"qiita/getUserItems",
async (userName: string) => {
const { data } = await axios.get<Item[]>(
`https://qiita.com/api/v2/users/${userName}/items`
);
return data;
}
);
/** Slice */
const qiitaSlice = createSlice({
name: "qiita",
initialState: initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchAsyncGetUserItems.fulfilled, (state, action) => {
return {
...state,
items: action.payload,
};
});
builder.addCase(fetchAsyncGetUserItems.rejected, (state, action) => {
return {
...state,
items: initialState.items,
};
});
},
});
// 各コンポーネントからstateを参照できるようにエクスポートをしておく
export const selectItems = (state: RootState) => state.qiita.items;
export default qiitaSlice.reducer;
今回の肝となるSliceの実装になります。1つずつ見ていきましょう。
-
まず、前半のinterface定義はAPIの戻り値の型になります。
-
次に非同期処理部分ですが、これはSliceの生成とは別に関数として定義します。
export const fetchAsyncGetUserItems = createAsyncThunk(
"qiita/getUserItems",
async (userName: string) => {
const { data } = await axios.get<Item[]>(
`https://qiita.com/api/v2/users/${userName}/items`
);
return data;
}
);
この時使用されるのがredux-toolkitで用視されている非同期要求関数createAsyncThunk
になります。
第一引数に任意の非同期処理名、第二引数に非同期ロジックのコールバック関数を渡します。
今回はQiitaのユーザーの記事一覧取得APIを対象にしていますが、ユーザー名だけ引数として受け取るようにしています。
- そしてSliceを作成している部分が以下になります。
ここではSliceの名前、保持するstateの初期値、Stateに対して許可する更新処理を定義していきます。
const qiitaSlice = createSlice({
name: "qiita",
initialState: initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchAsyncGetUserItems.fulfilled, (state, action) => {
return {
...state,
items: action.payload,
};
});
builder.addCase(fetchAsyncGetUserItems.rejected, (state, action) => {
return {
...state,
items: initialState.items,
};
});
},
});
こちらのextraReducers
に非同期処理のパターンをaddCase
で追加します。
ちなみに定義できるのは以下の3種類となります。
- pending: 保留中
- fulfilled: 成功
- rejected: 失敗
今回はAPIリクエストが成功した場合は戻り値でstateを更新するコールバック関数を、
失敗した場合は初期値でstateを更新するコールバック関数を定義しています。
最後にstoreに登録するreducerと、登録後に各コンポーネントからstateの値を参照するための関数をエクスポートしておきます。
// 各コンポーネントからstateを参照できるようにエクスポートをしておく
export const selectItems = (state: RootState) => state.qiita.items;
export default qiitaSlice.reducer;
このstate.qiita.items
のqiita
にあたる部分はこの後実装するstoreへの登録名になります。
以上でSliceの実装は完了です。
ここまでくればあと少しです!!
storeの実装
上記の createSlice で作成した各 Reducer を一つにまとめ、ストアを生成します。
環境構築の手順通りにアプリを作成した場合、storeのテンプレートはすでに作成済みかと思います。
今回追加した部分にはコメントを載せています。
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import qiitaReducer from "../features/qiita/qiitaSlice"; // 追加
export const store = configureStore({
reducer: {
qiita: qiitaReducer, // 追加
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
以上でstoreの実装は完了です。
コンポーネントの実装
最後にSliceで管理されているstateの参照や非同期処理の実行を定義します。
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchAsyncGetUserItems, selectItems } from "./qiitaSlice";
const Qiita: React.FC = () => {
const itemsData = useSelector(selectItems);
const dispatch = useDispatch();
useEffect(() => {
// 非同期処理を実行 // 引数に例として私のQiitaアカウント名を指定
dispatch(fetchAsyncGetUserItems("eiji-noguchi"));
}, [dispatch]);
const items = itemsData.map((item) => (
<div key={item.id}>
<p>{item.title}</p>
<a href={item.url}>{item.url}</a>
</div>
));
return <>{items}</>;
};
export default Qiita;
コンポーネント内からはuseSelector
をつかってstateの値を参照しています。
また、useDispatch
を使い非同期処理を実行しています。ここであuseEffect
と合わせて、最初の表示時に非同期処理を呼ぶようにしています。
ついでに取得したユーザーの記事のタイトル一覧を画面に表示してみましょう。
作成したQiitaコンポーネントをApp.tsx
に埋め込みます。
import "./App.css";
import Qiita from "./features/qiita/Qiita";
function App() {
return (
<div className="App">
<Qiita />
</div>
);
}
export default App;
以上で実装は完了になります。
アプリ実行
npm start
で実行してみましょう。こんな画面が出ればOKです。
まとめ
redux-toolkitを使った非同期処理を実装できるようになりました。
複雑になりがちな状態管理を定まったルール内で使えるようになるredux-toolkitはこれからのReact開発に必須なライブラリになると思われます。
正直、最初からすべてを理解するのは大変ですが、慣れるとReact開発において強力な一手になること間違いなしですね。