【Redux Toolkit】非同期データ取得、保持、加工をStoreに集約する

シングルページアプリケーションの開発に際し、データの取得、保持、加工をストアに任せる設計を経験した。実際に経験したのはNuxt3のPiniaだが、ReactのReduxでも同じことができるだろうということで忘備録として記録する。

参考にしたRedux公式ドキュメント: 『Redux Essentials, Part 7: RTK Query Basics』
https://redux.js.org/tutorials/essentials/part-7-rtk-query-basics

前提条件

Reactアプリケーションにおいて、Reduxが使用ができるようになっていること。
以下の記事をもとに解説する。

【Next.js14】「Redux」を使って状態管理を実現する手順

環境

"typescript": "^5"
"next": "14.0.3",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-hook-form": "^7.48.2",
"react-redux": "^9.1.0",
"@reduxjs/toolkit": "^2.2.3",

実装要件

(1)ページ: http://localhost:3000/test/redux/usersを用意する
(2)https://jsonplaceholder.typicode.com/usersからユーザー一覧を取得し、stateに同期する処理を作成する
(3)コンポーネントからデータ取得を実行する
(4)Reduxのデータを用いて、ユーザー一覧を描画する

ファイル群

// ユーザー情報の取得と保持を管理するストア
src/lib/redux/features/usersSlice.ts

// configureStoreを行なっているファイル
src/lib/redux/store.ts

// http://localhost:3000/test/redux/usersのコンポーネントファイル
src/pages/test/redux/users/index.tsx

src/lib/redux/features/usersSlice.ts

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store";

// ユーザー情報の型
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// ユーザー情報の取得処理
export const fetchUsers = createAsyncThunk("/users/fetchUsers", async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  return response.json();
});

export type UserSliceState = {
  data: User[];
  status: "idle" | "pending" | "succeeded" | "failed";
  error: undefined | string;
};

const initialState: UserSliceState = {
  data: [],
  status: "idle",
  error: undefined,
};

const userSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    initUsers: (state) => {
      state.data = [];
    },
  },
  // extraReducersにハンドラーを追加し、pending/fulfilled/rejectedのケースを処理
  extraReducers: (builder) => {
    builder.addCase(fetchUsers.pending, (state) => {
      state.status = "pending";
    });
    builder.addCase(fetchUsers.fulfilled, (state, action) => {
      state.data = action.payload;
      state.status = "succeeded";
    });
    builder.addCase(fetchUsers.rejected, (state, action) => {
      state.status = "failed";
      state.error = action.error.message;
    });
  },
});

export const selectUsers = (state: RootState) => state.users;

export const { initUsers } = userSlice.actions;

export default userSlice.reducer;

ポイントになるのはcreateAsyncThunk。非同期のデータフェッチにはcreateAsyncThunkを使用する。createAsyncThunkは、非同期処理の実行状況に応じてpendingfulfilledrejectedの3つのactionにアクセスできる。そのため、下記で記載しているように、fetchUsers.pendingfetchUsers.fulfilledfetchUsers.rejectedに応じて処理を分岐することができる。

src/lib/redux/store.ts

import { configureStore } from "@reduxjs/toolkit";
import { useDispatch, useSelector, useStore } from "react-redux";
import counterSlice from "./features/counterSlice";
// 追加
import usersReducer from "@/lib/redux/features/usersSlice";

export const reduxStore = () => {
  return configureStore({
    reducer: {
      counterStore: counterSlice,
      // 追加
      users: usersReducer,
    },
  });
};

export type AppStore = ReturnType<typeof reduxStore>;
export type RootState = ReturnType<AppStore["getState"]>;
type AppDispatch = AppStore["dispatch"];

export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppStore = useStore.withTypes<AppStore>();

src/pages/test/redux/users/index.tsx

import {
  fetchUsers,
  initUsers,
  selectUsers,
} from "@/lib/redux/features/usersSlice";
import { useAppDispatch, useAppSelector } from "@/lib/redux/store";
import {
  Alert,
  Box,
  Button,
  Container,
  List,
  ListItem,
  ListItemText,
} from "@mui/material";
import { useCallback } from "react";

export default function PhotoPage() {
  const dispatch = useAppDispatch();
  const users = useAppSelector(selectUsers);

  const handleClickGetUser = useCallback(() => {
    dispatch(fetchUsers()).catch((error) => error.message);
  }, [dispatch]);

  const handleClickInit = () => {
    dispatch(initUsers());
  };

  return (
    <Container maxWidth="md">
      <Button onClick={handleClickGetUser}>GET DATA</Button>
      <Button onClick={handleClickInit}>初期化</Button>

      <Box>
        {users.status === "failed" && (
          <Alert severity="error">This is an error Alert.</Alert>
        )}
      </Box>

      <Box>
        {users.status === "succeeded" && (
          <List>
            {users.data.map((item) => (
              <ListItem key={item.id}>
                <ListItemText primary={item.name}></ListItemText>
              </ListItem>
            ))}
          </List>
        )}
      </Box>
    </Container>
  );
}

createAsyncThunkで処理の状態をセットできるため、pending、fulfilled、rejectedの状態に応じて、表示を出し分けることができるようになる。

動作

以下の「GET DATA」を押下すると、

ユーザー情報の取得処理が走り、ユーザー一覧が表示される。

通信に失敗した場合は、アラートが表示される。

以上。

関連記事

コメント

この記事へのコメントはありません。