【React × Redux Toolkit】createAsyncThunkを使ってAPIを叩き、通信中はローディング画面を表示させる

記事タイトル

こんにちは、みつをです     

先日花火を見てきました

mitsuwo

エンジニア

みつを

暑さと人の多さで超疲れました

でも、きれいだったんで結果オーライです

 

mitsuwo

エンジニア

みつを

 

そんな話はさておき、早速本題に入るとします。

今回作るもの

今回は、JSONPlaceholderのAPIを叩いて、画像一覧を表示すると言った超簡単なアプリを作っていきます。

データの取得が終わるまではローディング中というのがわかるように、ローディング画面が表示されるよう実装を進めていきます。

環境構築

今回の実行環境はこんな感じです。

・react 18.2.0 (typescript)

・react-redux 8.0.2

・redux toolkit 1.8.5

 

ターミナルで下記コマンドを打ってアプリの雛形を作成します。

ちなみに「my-app」ってところがアプリ名になります。好きな名前を入れましょう。

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

これだけで準備完了です!

早速作っていきましょう!

 

画像一覧表示アプリを作ろう

初期化

ひとまず現在の状態で起動してみましょう。

ターミナルで下記コマンドを打って起動します。

yarn start

カウンターのアプリケーションが立ち上がれば問題なく起動できています。

ただ、今回はこのカウンターアプリは使わないので、App.tsxをすっからかんにします。

import React from 'react';
  import './App.css';
  
  function App() {
    return (
      <div className="App">
      </div>
    );
  }
  
  export default App;

ひとまず初期化はこれでOK。

 

Sliceを作る

src/features/に新しくphotosフォルダを作成し、その中にphotoSlice.tsを作成します。

中身は以下の通りです。

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
  import { RootState } from "../../app/store";
  
  export const fetchPhotos = createAsyncThunk("/photos/fetchPhotos", async () => {
      const response = await fetch("https://jsonplaceholder.typicode.com/photos");
      return response.json();
  });
  
  const photoDataExample = {
    albumId: 1,
    id: 1,
    title: "accusamus beatae ad facilis cum similique qui sunt",
    url: "https://via.placeholder.com/600/92c952",
    thumbnailUrl: "https://via.placeholder.com/150/92c952",
  };
  
  export type PhotosData = {
    data: typeof photoDataExample[];
    status: "idle" | "pending" | "succeeded" | "failed";
    error: undefined | string;
  };
  
  const initialState: PhotosData = {
    data: [],
    status: "idle",
    error: undefined,
  };
  
  export const photoSlice = createSlice({
    name: "photo",
    initialState,
    reducers: {},
    extraReducers: (builder) => {
      builder.addCase(fetchPhotos.pending, (state) => {
        state.status = "pending";
      });
      builder.addCase(fetchPhotos.fulfilled, (state, action) => {
        state.data = action.payload;
        state.status = "succeeded";
      });
      builder.addCase(fetchPhotos.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
    },
  });
  
  export const selectPhotos = (state: RootState) => state.photos;
  export default photoSlice.reducer;

initialState

このinitialStateという定数では、stateの初期状態を指定しています。

export type PhotosData = {
    data: typeof photoDataExample[];
    status: "idle" | "pending" | "succeeded" | "failed";
    error: undefined | string;
  };
  
  const initialState: PhotosData = {
    data: [],
    status: "idle",
    error: undefined,
  };

dataという空の配列がありますが、ここには後々APIで取ってくる画像のデータが格納されます。

statusではローディング状態を管理しています。今回はこのstatusを使ってローディング画面表示の切り替えを行います。

 

createAsyncThunk

export const fetchPhotos = createAsyncThunk("/photos/fetchPhotos", async () => {
      const response = await fetch("https://jsonplaceholder.typicode.com/photos");
      return response.json();
  });

ここで来ました本日の主役です。

このfetchPhotosでapiを叩くのですが、ここで使うのが createAsyncThunk です。

createAsyncThunkは非同期処理の実行状況に応じて、

  • pending
  • fulfilled
  • rejected

上記3つのactionを生成してくれます。

第一引数にはアクションの名前(タイプ)、第二引数には非同期処理の関数を取ります。

(厳密に言うと第一引数はtype、第二引数はpayloadActionを受け取ります。)

 

今回アクションの名前は/photos/fetchPhotosにしてます。

非同期処理の関数はJSONPlaceholderのphotosを取得できるAPIを叩いています。

 

createSlice – extraReducers

createSliceでpostSliceを作成します。

export const photoSlice = createSlice({
    name: "photo",
    initialState,
    reducers: {},
    extraReducers: (builder) => {
      builder.addCase(fetchPhotos.pending, (state) => {
        state.status = "pending";
      });
      builder.addCase(fetchPhotos.fulfilled, (state, action) => {
        state.data = action.payload;
        state.status = "succeeded";
      });
      builder.addCase(fetchPhotos.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
    },
  });
  
  export const selectPhotos = (state: RootState) => state.photos;
  export default photoSlice.reducer;

「name」にはsliceの名前を入力し、その次に初期値を指定します。さっき作ったinitialStateはここで使います。

その下にある「reducers: {}」ですが、今回は通常のreducerは使わないので空のままでいいです。

そして次に出てくるのが extraReducers です。

ここではさっきcreateAsyncThunkで生成された3つのアクション、pending、fulfilled、rejectedに対するstateの変化を書いていきます。

 

pending時はstate.status"pending"に変更します。

fulfilled時はstate.status"succeeded"に変更し、取得したデータをstate.dataに入れます。

failed時はstate.status"failed"とし、state.errorでエラーメッセージを受け取ります。

 

次に、src/app/store.tsを開いて、コードを以下の通り修正します。

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
  import photoReducer from '../features/photos/photoSlice';
  
  export const store = configureStore({
    reducer: {
      photos: photoReducer
    },
  });
  
  export type AppDispatch = typeof store.dispatch;
  export type RootState = ReturnType<typeof store.getState>;
  export type AppThunk<ReturnType = void> = ThunkAction<
    ReturnType,
    RootState,
    unknown,
    Action<string>
  >;

これで今回作成したphotoSliceがstoreで管理できるようになりました。

コンポーネントを作る

redux周りの処理が完成したので、早速コンポーネントを作ってみましょう。

photoSlice.tsと同階層にPhotos.tsxを作成します。コードは以下の通りです。

import React, { useCallback } from "react";
  import { useAppSelector, useAppDispatch } from "../../app/hooks";
  import { fetchPhotos, selectPhotos } from "./photoSlice";
  import styles from "./Photos.module.css";
  
  export const Photos: React.FC = () => {
    const dispatch = useAppDispatch(); // 1
    const photos = useAppSelector(selectPhotos); // 2
  
  // 3
    const handleClick = useCallback(() => { 
      dispatch(fetchPhotos()).catch((error) => error.message);
    }, [dispatch]);
  
    return (
      <div>
        <button onClick={handleClick}>GET</button>
        {photos.status === "succeeded" && ( // 4
          <ul className={styles.photoUl}>
            {photos.data.map((element) => (
              <li key={element.id} className={styles.photoLi}>
                <img src={element.url} alt={element.title} />
              </li>
            ))}
          </ul>
        )}
        {photos.status === "pending" && <div>Loading...</div>} // 5
        {photos.status === "failed" && <div>{photos.error}</div>} // 6
      </div>
    );
  };

コメントで番号を振ってあるので、一つずつ解説していきます。

  1. useAppDispatchを実体化しています。dispatchしたい時はこのように実体化してから実行します。
  2. useAppSelectorを使うことで、storeのstateを参照できます。今回はphotoSliceの方でslice.photosをselectPhotosという名前でexportしているのでそれを使います。
  3. ボタンを押したらAPIを叩くように、関数を作成しています。createAsyncThunkで作ったfetchPhotosをdispatchしています。
  4. データの取得が完了したらstatus"succeeded"になり、画像一覧が表示されます。
  5. データの取得状況がpendingの場合、status"pending"となり、その間は「Loading…」という文字だけを返すようにしています。今回は仮で<div>Loading...</div>としていますが、ここにローディングスピナーコンポーネントや、スケルトンコンポーネントを表示させることで、リッチな見た目にできます。
  6. データ取得が失敗すれば、エラーが表示されるようにしています。

 

最後にこのPhotosコンポーネントをApp.tsxで表示させれば完成です。

完成品

簡単に完成したものを見てみましょう。(スタイリングは適当につけてます)

 

初期画面でGETボタンを押すと…

 

 

Loading…と表示されていますね。

 

データの取得が完了し、画像が一覧表示されました。

これで完成です!

終わりに

今回使ったRedux Toolkitですが、普通のReduxに比べるとかなり簡単に使えるのでおすすめです!

「API通信中もユーザー体験を良くしたい」といった方はこれを機にぜひお試しください!

 

ロジカルスタジオではフロントエンジニアを募集しています。

今回使ったReact等、モダンなウェブフロント技術を使って開発がしてみたいという方は、是非採用サイトからご応募ください!