1
/
5

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


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

今回作るもの

今回は、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通信中もユーザー体験を良くしたい」といった方はこれを機にぜひお試しください!


ロジカルスタジオではテクニカルディレクターを募集しています。

モダンなウェブフロント技術に興味のある方はぜひご応募ください!

フロントエンドエンジニア
即戦力募集!問題を解決するテクニカルディレクターをWanted!
ロジカルスタジオは、テクノロジーとデザインの視点からクライアントの課題解決をめざす、大阪のクリエイティブプロダクションです。 クライアントの期待を超えて「もっと良くなる、を見つける」のが私たちのスタイル。企画・提案からデザイン制作・システム構築、運用、改善提案をワンストップで行えるのが一番の強みです。近年ではその実績と品質が評価され、案件も急増しています。 代表の古川が掲げる企業理念は「絆を大切に、周りの人を豊かにし、社会に貢献する」。 私たちはこのミッションを推進するため、「新しい技術へのチャレンジ精神」を軸に、「切磋琢磨する仲間」と「働きやすい職場環境」を整え、「成長と可能性を大切にする風土」を育ててきました。 2019年9月に増床し、外部向けのセミナーにも活用できるカンファレンスルームを拡充。スタッフによる公式ブログや、マスコットキャラクター「ロージー&カール」によるTwitterなど、情報発信も強化しています。 可能性の芽をすくい上げられるこの場所を、より大きくしていきたい。 私たちと一緒に、このビジョンを実現しませんか。
株式会社ロジカルスタジオ
Invitation from 株式会社ロジカルスタジオ
If this story triggered your interest, have a chat with the team?
株式会社ロジカルスタジオ's job postings
11 Likes
11 Likes

Weekly ranking

Show other rankings
Like 岩村 充基's Story
Let 岩村 充基's company know you're interested in their content