redux-toolkitで非同期処理を扱う
本記事は Redux-toolkitを使ったStoreをtypescriptで作ってみる の続きです。
掲載しているコードの中には無駄にコメントアウトされている部分が残っていますが、これは上記記事との差異を明確にするために残しているものです。
参考
以下の記事を参考にさせていただきました。
PAGE TITLE | MEMO(参考にした部分) | LINK |
---|---|---|
createAsyncThunk | 公式の非同期実装パターン解説 | https://redux-toolkit.js.org/api/createAsyncThunk |
【React/Redux】「Redux Tool Kit」で非同期のSliceを作る場合 | 非公式の非同期Slice実装パターン解説 どこに何を書くなど非常に参考になる |
http://www.code-magagine.com/?p=13509 |
Redux Hooks によるラクラク dispatch & states | useDispatchとuseCallbackの組み合わせかたについて | https://qiita.com/Ouvill/items/569384e5c8c7ce78f98e |
React hooksを基礎から理解する (useCallback編+ React.memo) | useCallback, useMemoそれぞれの機能について | https://qiita.com/seira/items/8a170cc950241a8fdb23 |
非同期のSlice実装
同期型のSliceではreducerの中に関数を実装していきましたが、非同期処理はcreateSliceの外にcreateAsyncThunkで処理を実装し、extraReducersで実装した関数を組み込むような実装が推奨されています。
コードを追いながら見ていきたいと思います。
パッケージ群のインストール
今回は外部APIから情報を取得しようと思うのでaxiosをインストールします。
また、扱う情報が今回多くなるので簡単にコンポーネントを実装できるようMUIを追加します。
$ yarn add -E axios
$ yarn add @mui/material @emotion/react @emotion/styled @mui/lab -E
sliceを実装する
今回はzennのトレンドを取得するAPIを見つけたので、こちらを使用させていただきます。
-
src/zennApiSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import axios, { AxiosResponse } from 'axios'; import { ZennTrendItem } from './type/zennTrend'; const base = 'https://zenn-api.netlify.app/.netlify/functions'; type State = { trends: Array<ZennTrendItem>; loading: boolean; error: { status: boolean; message: string | null; }; }; const initialState: State = { trends: [], loading: false, error: { status: false, message: null, }, }; export const fetchZennTrend = createAsyncThunk('zennApi/fetchZennTrend', async () => { const result: AxiosResponse<Array<ZennTrendItem>> = await axios.get(`${base}/trendTech`); return result.data; }); export const zennApiSlice = createSlice({ name: 'zennApi', initialState, reducers: {}, extraReducers: (builder) => { builder.addCase(fetchZennTrend.pending, (state, action) => { state.loading = true; state.error.status = false; state.error.message = null; }); builder.addCase(fetchZennTrend.fulfilled, (state, action) => { state.loading = false; state.trends = new Array(...action.payload); }); builder.addCase(fetchZennTrend.rejected, (state, action) => { state.loading = true; state.error.status = true; state.error.message = 'cant fetched data from zenn api(unofficial)'; }); }, }); export default zennApiSlice.reducer;
-
src/type/zennTrend.ts
※ 本部分についてはAPIの返り値から勝手に推測して型を作っています。export type ZennTrendItem = { id: number; postType: string; title: string; slug: string; published: boolean; commentsCount: number; likedCount: number; bodyLettersCount: number; readingTime: number; articleType: string; emoji: string; isSuspendingPrivate: boolean; publishedAt: string; bodyUpdatedAt: string; sourceRepoUpdatedAt: null; createdAt: string; updatedAt: string; user: { id: number; username: string; name: string; avatarSmallUrl: string; }; publication: null; topics: { id: number; name: string; displayName: string; taggingsCount: number; imageUrl: string; }[]; }; export type ZennTrends = Array<ZennTrendItem>;
具体的には、 fetchZennTrend
が処理関数になっており、ここでAPIを実行しています。
続いて、 zennApiSlice
内の extraReducer
で非同期処理のステータスに対するハンドリングを行っています。
method | 意味 |
---|---|
pending | 非同期実行中、または実行待ち |
fulfilled | 実行が正常完了(エラーなし) |
rejected | 実行で異常発生 |
これらのステータスに対する挙動を builder.addCase
を使って設定している感じ。
注意
ソース中のエラーに対する処理などはあまり参考になりません。
エラーハンドリングなどは別途記事を書きたいと思っていますが、あくまでこんなことができる程度の認識にとどめてください。
storeにslice, selectorを追加する
-
src/store.ts
import { configureStore } from '@reduxjs/toolkit'; import { useSelector as rawUseSelector, TypedUseSelectorHook } from 'react-redux'; import counterReducer from './counterSlice'; import zennApiReducer from './zennApiSlice'; export const store = configureStore({ reducer: { counter: counterReducer, zennApi: zennApiReducer, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export const useSelector: TypedUseSelectorHook<RootState> = rawUseSelector; export const zennTrendSelector = createSelector(zennApiSelector, (zenn) => { return zenn.trends; });
componentから呼び出してみる
実装
-
src/App.tsx
import './App.css'; import { FC, useCallback, useEffect, useState } from 'react'; import { AppDispatch, useSelector, zennTrendSelector } from './store'; import { useDispatch } from 'react-redux'; import { additional, subtraction } from './counterSlice'; import { ZennTrendItem } from './type/zennTrend'; import { Avatar, Box, Card, CardActions, CardContent, CardHeader, Grid, Typography } from '@mui/material'; import { fetchZennTrend } from './zennApiSlice'; const TrendCard: FC<{ item: ZennTrendItem }> = ({ item }) => { return ( <Grid item xs={6}> <Card> <CardHeader avatar={<Avatar aria-label="recipe">{item.emoji}</Avatar>} title={item.title} sx={{ display: 'flex', justifyContent: 'start', textAlign: 'start' }} /> <CardActions> <CardContent sx={{ display: 'flex', justifyContent: 'start', textAlign: 'start' }}> <Typography variant="body2" color="text.secondary"> user: {item.user.name} <br /> category: {`${item.topics.map((t) => t.displayName)}`} </Typography> </CardContent> </CardActions> </Card> </Grid> ); }; const App: FC = () => { const count = useSelector((state) => state.counter.count); const dispatch: AppDispatch = useDispatch(); const trends = useSelector(zennTrendSelector); const fetchTrends = useCallback(() => { dispatch(fetchZennTrend()); }, [dispatch]); useEffect(() => { if (trends.length === 0) return fetchTrends(); return; }, [fetchTrends, trends]); // const [count, setCount] = useState<number>(0); // const addition = (num: number) => { // if (Number.isNaN(num)) return; // setCount(count + num); // }; // const subtraction = (num: number) => { // if (Number.isNaN(num)) return; // setCount(count - num); // }; return ( <div className="App"> <Box sx={{ m: 2 }}> <h1>Count: {count}</h1> {/* <button onClick={() => addition(1)}>Up</button> <button onClick={() => subtraction(1)}>Down</button> */} <button onClick={() => dispatch(additional(1))}>Up</button> <button onClick={() => dispatch(subtraction(1))}>Down</button> {trends.length === 0 ? ( <></> ) : ( <Grid container justifyContent="center" alignItems="center" sx={{ display: 'flex' }} spacing={2}> {trends.map((item) => { return <TrendCard item={item} key={item.id} />; })} </Grid> )} </Box> </div> ); }; export default App;
説明
重要なのは以下の部分です。
const trends = useSelector(zennTrendSelector);
const fetchTrends = useCallback(() => {
dispatch(fetchZennTrend());
}, [dispatch]);
useEffect(() => {
if (trends.length === 0) return fetchTrends();
return;
}, [fetchTrends, trends]);
- trendsで、storeに格納しているstateを取得しています。
- fetchTrendsで sliceの非同期処理(action)を実行しています。この時、useCallbackを使用することで不必要に再描画が走らないよう予防しています。
(stateが変わる時だけ再描画) - useEffectで画面が描画された段階でtrends(store)にデータが入っていない場合にfetchZennTrendを実行するようにしています。
さいごに
今回実装しているsliceは本来reduxを通す必要は全くないということは理解しています。
非同期に関して紹介するのであれば本来はログイン処理やログイン後のユーザーセッティングなどについてをテーマにすべきですが、ログイン機能についてやその他ライブラリの紹介をせずに非同期で処理するというテーマが浮かばなかったため今回のような内容になっています。
ただ、createAsyncThunkの使い方はおおよそわかる内容になったと思うので、参考にできるところは参考にしていただけるとと思います。
Discussion