🌟

React + Redux / Redux Toolkitを使った非同期通信の検証

に公開

概要

今回はWebアプリにおいて重要な要素となる非同期通信を、Reduxを使って実装してみようと思います。
本記事を書くにあたって色々調べて見ましたが、シンプルに非同期通信と言ってもその実装方法は多角的で如何様にもできるのではという印象を感じました。
また、よく目にした createStore を使った実装方法は現時点で非推奨となっていたり、学習要素とトレンド要素が情報として混在していたりと自分にとっての落とし所を見つけるのにも相当苦労した経緯があります。
あくまで一見解という形で見ていただけると幸いです。

実装機能

  • Reduxを使ったstore, reducer, actionの実装
  • バックエンドで実装したAPIエンドポイントの読み込み〜表示
項番 記事
1 React + Django + CORSを使ったフロントエンド / バックエンドのデータ連携
2 Django 管理画面のカスタマイズ
3 Django REST framework(DRF)を使ったAPIサーバーとReactとのデータ連携
4 Django REST frameworkのserializersを使った外部キーモデルの参照
5 React + Redux / Redux Toolkitを使った非同期通信の検証(本記事)
6 APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(設計編)(後日公開)
7 APIをテストツール「Postman」を使ったDjangoとのCRUD機能実装(実装編)(後日公開)

フォルダ構成

今回使用しているものをメインに抜粋

  • フロントエンド(React / JavaScript)
[Reactプロジェクト]
├── src
    ├── components
          :
    ├── features  # 新規追加
    │   ├── mypage
    │   │   └── mypageSlice.js
    ├── pages
    │   └── Mypage.js
    ├── reducers  # 新規追加
    │   └── index.js
    └── store  # 新規追加
    │   └── index.js
    ├── BaseApp.css
    ├── BaseApp.js
    └── index.js

Reduxのセットアップ

公式サイトと、その中で使用を推奨されているRedux Toolkitを併せて紹介します。

  • Redux Core
# NPM
npm install redux

# Yarn
yarn add redux
  • Redux Toolkit
# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

Reactで使う場合は React-Redux もインストールしておきます。

npm install react-redux

実装方法

フロントエンド

まず今回実装する関数をまとめて紹介します。

  • Redux
    • useEffect: コンポーネントのレンダリング後に特定の処理を実行するためのhooks
    • useSelector: storeのstateを参照するためのhooks
    • useDispatch: storeのstateを更新するためのhooks
    • combineReducers: 複数のreducerを結合してstoreに渡す
  • Redux Toolkit

storeはプロジェクト全体で保有できるデータのことで、親子コンポーネントの橋渡しとなっているpropsやコンポーネントが独自で持っているstateのみでやり取りしていたバケツリレーのような状態を解消できるものです。

Store / Reducer実装

参照先はJSONデータを無料で取得できるJSONPlaceholderにしています。

/src/features/mypage/mypageSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const initialState = {
  isLoading: false,
  items: [],
};

const BASE_API_URL = "https://jsonplaceholder.typicode.com";

/** データ取得非同期処理 */
export const fetchAccountList = createAsyncThunk(
  "mypage/getAccountList",  // actionの名前
  async (id) => {  // 非同期処理を行う関数を指定
    const response = await axios.get(`${BASE_API_URL}/users/${id}`);
    return response.data;
  }
);

// Slices
export const accountSlice = createSlice({
  name: "account",  // stateの名前
  initialState: initialState,  // stateの初期値
  reducers: {
    getAccountList: (state, action) => {
      return {
        ...state,
        items: action.payload,
      };
    },
  },
  // 作成したreducerに対して、タイミングに応じて追加でreducerを作成するためのhooks
  extraReducers: (builder) => {
    builder
      .addCase(fetchAccountList.pending, (state) => {
        return {
          ...state,
          isLoading: true,
        };
      })
      .addCase(fetchAccountList.fulfilled, (state, action) => {
        return {
          ...state,
          items: action.payload,
          isLoading: false,
        };
      })
      .addCase(fetchAccountList.rejected, (state) => {
        return {
          ...state,
          isLoading: false,
        };
      });
  },
});

// 各コンポーネントからstateを参照できるようにエクスポートをしておく
export const { getAccountList } = accountSlice.actions;
export default accountSlice.reducer;

/src/reducers/index.js
import { combineReducers } from 'redux';

// export defaultで取得したreducerを任意名でインポート
import accountReducer from '../features/mypage/mypageSlice';
  :

const rootReducer = combineReducers({
  // ここに作成したReducerを記述する
  accountReducer,
});

export default rootReducer;
/src/store/index.js
import { configureStore } from "@reduxjs/toolkit";

import rootReducer from "../reducers/index";

const store = configureStore({
  reducer: rootReducer,
});

export default store;

コンポーネント

store管理はアプリ全体で行うため、ルートコンポーネントで呼ぶ形にするのが最適です。

/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import BaseApp from './BaseApp';
import { Provider } from 'react-redux';
import store from './store';

import { BrowserRouter } from 'react-router-dom'
  :

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
        <BrowserRouter>
            <BaseApp />
        </BrowserRouter>
    </Provider>
  </React.StrictMode>
);

BaseApp > Mypageの構成でコンポーネントを読み込みます。
Mypageにpropsで受け渡した値を元に、読み込み時にAPIを参照してJSONで返ってきたデータを表示させるという流れになります。
また getAccountList は使っていませんが、何かしらのユーザー操作が入った時にdispatchしてstoreを更新できるようになります。

デザイン実装にはMaterial UI(MUI)を使っていますが、詳細は割愛します。

/src/pages/Mypage.js
import React, { useEffect } from 'react'

import { Link } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { Box, Container, Grid, Typography, Breadcrumbs, Button, Table } from '@mui/material'
import { TableContainer, TableRow, TableCell, TableBody, Paper } from '@mui/material'
import NavigateNextIcon from '@mui/icons-material/NavigateNext';

import { fetchAccountList, getAccountList } from '../features/mypage/mypageSlice'

const Mypage = (props) => {

  const user_id = props.user_id;

  const itemsAccount = useSelector((state) => state.accountReducer.items);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchAccountList(user_id));
  }, [dispatch, user_id]);

  const usersList = itemsAccount;
  
  const breadcrumbs = [
    { name: 'ホーム', href: '/dashboard/' },
    { name: 'マイページ' },
  ];    

  return (
    <Container className='page-maincontents'>
        :

      <Box className='section-wrapper'>
        <Grid container className='section-header'>
          <Grid item className='section-title'>
            ユーザープロフィール
          </Grid>
        </Grid>

        <TableContainer component={Paper}>
          <Table sx={{ minWidth: 650 }} aria-label="simple table">
            <TableBody>
              <TableRow >
                <TableCell component="th" scope="row">
                  ID
                </TableCell>
                <TableCell align="right">{usersList.id}</TableCell>
              </TableRow>
              <TableRow >
                <TableCell component="th" scope="row">
                  氏名
                </TableCell>
                <TableCell align="right">{usersList.name}</TableCell>
              </TableRow>
              <TableRow >
                <TableCell component="th" scope="row">
                  ユーザー名
                </TableCell>
                <TableCell align="right">{usersList.username}</TableCell>
              </TableRow>
              <TableRow >
                <TableCell component="th" scope="row">
                  Email
                </TableCell>
                <TableCell align="right">{usersList.email}</TableCell>
              </TableRow>
              <TableRow >
                <TableCell component="th" scope="row">
                  電話番号
                </TableCell>
                <TableCell align="right">{usersList.phone}</TableCell>
              </TableRow>
              <TableRow >
                <TableCell component="th" scope="row">
                  Webサイト
                </TableCell>
                <TableCell align="right">{usersList.website}</TableCell>
              </TableRow>
            </TableBody>
          </Table>
        </TableContainer>

          :
      </Box>
    </Container>
  )
}

export default Mypage

実装ビュー・まとめ

スクリーンショット 2024-01-25 17.52.06.png

ローカルで画面のURLにアクセスした際の読み込みの早さを実感できたので、実際のユーザー操作が入った場合でも優れたパフォーマンスになると思います。
ファイル構成はReduxの公式サイトに近い構成を意識しましたが、インポート / エクスポート組み方さえ合えばいくらでもやりようがあるとも感じました。
ここはプロジェクトの仕様によりけりなので、上流工程で着手する際には本来の正しい使い方になるよう意識しようと思いました。

参考文献

  • Getting Started with Redux

https://redux.js.org/introduction/getting-started

  • React Hooks + Redux ToolKit (+ TypeScript + axios)でQiitaAPIをたたく(非同期処理)

https://qiita.com/eiji-noguchi/items/484e3b9407b1744a5467

  • Reduxを最低限だけやる【2020年版】

https://zenn.dev/nbr41to/articles/52360e6db88cfc290d1a

  • Reduxの全体像をざっくり理解する奴

https://zenn.dev/yukiji/articles/8a69f1615e6705

Discussion