1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Redux Toolkit サクッと入門

Posted at

はじめに

Redux Toolkit とは、アプリケーションの状態管理をする Redux をより簡単に扱えるようにしたライブラリです。公式 にもあるように、Redux の公式推奨ライブラリとなっています。
従来の Redux 記法における問題点として挙げられる「ストアのセットアップが煩雑」「不変性を意識した State の更新処理(スプレッド構文の多用)」等を解決し、よりシンプルに Redux を扱えるようになっています。

本記事の流れ

今回のトピックはこちらです。

  • ストアの作成
  • State の参照
  • State の更新
  • API 通信を伴う State の更新

動作環境はこちらになります。

  • Vite: 5.0.8
  • React: 18.2.0
  • TypeScript: 5.2.2

Redux Toolkit 関連のライブラリもインストールしておきます。

$ yarn add @reduxjs/toolkit react-redux
"@reduxjs/toolkit": "^2.0.1",
"react-redux": "^9.0.4",

では、本題に入ります。

ストアの作成

まずは、スライスを作成します。
スライスとは、Reducer や Action をまとめたものです。

スライスの作成には、createSlice を使います。
createSlice に State(初期状態)と Reducer 関数をまとめて渡してあげることでスライスが作成されます。

import { createSlice } from "@reduxjs/toolkit";

export const USER_SLICE_NAME = "user";

type UserState = {
  id: string;
  firstName: string;
  lastName: string;
};

const initialState: UserState = {
  id: "1",
  firstName: "John",
  lastName: "Doe",
};

const userSlice = createSlice({
  name: USER_SLICE_NAME, 
  initialState,
  reducers: {
    changeFirstName: (state, action) => {
      state.firstName = action.payload;
    },
    changeLastName: (state, action) => {
      state.lastName = action.payload;
    },
  },
});

export const { changeFirstName, changeLastName } = userSlice.actions;
export const userReducer = userSlice.reducer;

name にはスライスの名前が入ります。ストアから State を取得する際のキーとして使用します。reducers には、Reducer 関数(State を更新する処理)を記述します。

スライスを作成すると、内部で Action Creator と Action Type も自動的に生成されます。Action Type は {type: "user/changeFirstName"} の様な形式となり、スライスの名前がプレフィックスになっています。

以上で、スライスの作成は完了。どんなアクションで、どのように State を更新するか定義できました。

続いて、ストアの作成を行いますが、その前に、従来の Redux 記法と比較してみます。

// 従来の Redux で書いた場合

// Action Type
const CHANGE_FIRST_NAME = 'CHANGE_FIRST_NAME'
const CHANGE_LAST_NAME = 'CHANGE_LAST_NAME'

// Action Creator
const changeFirstName = (firstName: string) => ({
  type: CHANGE_FIRST_NAME,
  payload: firstName
})

const changeLastName = (lastName: string) => ({
  type: CHANGE_LAST_NAME,
  payload: lastName
})

const initialState = {
  id: "1",
  firstName: "John",
  lastName: "Doe",
};

// Reducer
const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_FIRST_NAME:
      return {
        ...state,
        firstName: action.payload
      }
    case CHANGE_LAST_NAME:
      return {
        ...state,
        lastName: action.payload
      }
    default:
      return state
  }
}

export { changeFirstName, changeLastName, userReducer }

Redux Toolkit の方がスッキリしているかと思います。また、スプレッド構文も省略できています。これは、Redux Toolkit 内部で Immer が使用されていることにより、Redux State の不変性を意識した更新処理が不要になったからです。

本題に戻ります。
次に、configureStore を使って、ストアを作成します。先程作成した Reducer を渡してあげます。

import { configureStore } from "@reduxjs/toolkit";
import { userReducer } from "path/to/user-slice";

const store = configureStore({
  reducer: {
    user: userReducer,
    // hoge: hogeReducer, ← 作成した Reducer はここに追加していく
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

store.getState はストアの State を返すメソッドです。つまり、RootState は State をまとめた型となります。

type RootState = {
  user: UserState;
  hoge: HogeState;
}

RootState は State 取得時に使用します。
AppDispatch は、dispatch 関数の型です。dispatch 関数作成時に使用します。

最後に、ストアを Provider に渡してセットアップ完了です。

import { Provider } from "react-redux";
import store from "path/to/store";

function App() {
  return (
    <Provider store={store}>
      {/* ... */}
    </Provider>
  );
}

export default App;

State の参照

State の参照には、useSelector フックを使います。
useSelector のインターフェースの定義は以下です。

// Interface
<TState = unknown, Selected = unknown>(selector: (state: TState) => Selected, equalityFn?: EqualityFn<Selected>): Selected;

第1引数にはセレクター関数を渡し、どの State を取得するか指定します。また、ストア作成時に定義した RootState を使って型付けも行います。

import { useSelector } from "react-redux";
import { RootState } from "path/to/store";
import { USER_SLICE_NAME } from "path/to/user-slice";

const userState = useSelector<RootState, RootState[USER_SLICE_NAME]>((state) => state.user);
console.log(userState.firstName, userState.lastName); // John Doe

以上で、useSelector を使った State の参照は完了です。
アクションが発行されると、前のセレクター結果値と新しいセレクター結果値の厳密等価比較(===)を行い、変更があればコンポーネントが再レンダリングされます。

State の更新

State を更新するには、アクションを発行し、 Reducer を呼び出す必要があります。
Reducer は、発行されたアクションの Action Type に応じて、State を更新します。

まずは、アクションを発行するための Dispatch 関数を作成します。Dispatch 関数の作成には、useDispatch フックを使います。

import { useDispatch } from "react-redux";
import { AppDispatch } from "path/to/store";

const dispatch = useDispatch<AppDispatch>();

ストア作成時に定義した AppDispatch を使って型付けも行っています。
Dispatch 関数の作成から、アクション発行までの一連の流れです。

// Usage
const Form = () => {
  const dispatch = useDispatch<AppDispatch>();

  const dispatchChangeFirstName = useCallback(
    (firstName: string) => {
      dispatch(changeFirstName(firstName));
    },
    [dispatch]
  );

  return (
    <div>
      <input
        type="text"
        value={firstName}
        onChange={(e) => dispatchChangeFirstName(e.target.value)}
      />
    </div>
  );
};

dispatch(changeFirstName(firstName)); の部分でアクションを発行しています。アクションは以下の形式で発行されます。

{
  "type": "user/changeFirstName",
  "payload": "<input value>"
}

API 通信を伴う State の更新

非同期に API 通信を行い、その結果を State に反映させます。
Redux には非同期処理の手法がいくつかありますが、今回は、createAsyncThunk 関数を使って非同期処理を行います。

createAsyncThunk は、非同期リクエストのライフサイクルを管理するための関数です。
引数に、Action Type と非同期処理を受け取り、Action Creator を生成します。

import { createAsyncThunk } from "@reduxjs/toolkit";

type UserResponse = {
  id: string;
  firstName: string;
  lastName: string;
};

export const findUser = createAsyncThunk<UserResponse, string>(
  "user/findUser", // Action Type
  async (userId) => { // Async Function
    const response = await fetch(`https://example.com/users/${userId}`);
    const data = await response.json();
    return data;
  }
);

第2引数(非同期処理)は、payloadCreator と呼ばれ、名前の通り、アクションの Payload を作成します。つまり、findUser 起点のアクションは以下になります。

{
  "type": "user/findUser/pending",
  "payload": "<Result of async function>"
}

Action Type の末尾に pending という文字列が急に出てきましたが、これは非同期処理の状況を表すものです。処理状況は、pending(非同期処理中)、fulfilled(非同期処理の成功)、rejected(非同期処理の失敗) の3種類があります。

前述した通り、createAsyncThunk は、非同期リクエストのライフサイクルを管理できます。そのため、処理状況に応じて処理を切り替えることが可能で、 pending の時はローディング画面、rejected の時はエラー画面を表示するといったことができます。

では、続いて、非同期処理の状況に応じて処理を切り替えてみます。
まず初めに、Reducer に処理を追加する必要があります。と言うのも、現時点では、createAsyncThunk によりアクションの準備はできたものの、そのアクションを受け取り、State を更新してくれる Reducer が存在しないからです。

「ストアの作成」で作成したスライスに Reducer を追加します。
createAsyncThunk など、スライス外で定義したアクションは extraReducers に追加します。

type User = {
  id: string;
  firstName: string;
  lastName: string;
};

type Status = "idle" | "loading" | "succeeded" | "failed";

type UserState = {
  data: User;
  status: Status;
};

const initialState: UserState = {
  data: {
    id: "1",
    firstName: "John",
    lastName: "Doe",
  },
  status: "idle",
};

const userSlice = createSlice({
  name: USER_SLICE_NAME, 
  initialState,
  reducers: {
    // ...
  },
  extraReducers: (builder) => { // 追加
    builder
      .addCase(findUser.pending, (state) => {
        state.status = "loading";
      })
      .addCase(findUser.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.data = action.payload;
      })
      .addCase(findUser.rejected, (state) => {
        state.status = "failed";
      });
  },
});

builder.addCase により、アクションと Reducer をマッピングしています。
また、State(UserState) の型も変更し新たに status を保持するようにしています。status は画面表示の切り替えに使用します。

State の内容に応じて画面表示を切り替えてみます。

import { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "path/to/store";
import { USER_SLICE_NAME } from "path/to/user-slice";
import { findUser } from "path/to/user-async-thunk";

const UserDetail = () => {
  const userState = useSelector<RootState, RootState[USER_SLICE_NAME]>(
    (state) => state.user
  );
  const dispatch = useDispatch<AppDispatch>();

  useEffect(() => {
    dispatch(findUser("1"));
  }, [dispatch]);

  return (
    <div>
      {userState.status === "loading" && <div>Loading...</div>}
      {userState.status === "failed" && <div>Something went wrong!</div>}
      {userState.status === "succeeded" && (
        <div>
          <div>{userState.data.firstName}</div>
          <div>{userState.data.lastName}</div>
        </div>
      )}
    </div>
  );
};

これで非同期処理(API 通信)の状況に応じて画面表示を切り替えることができます。

以上です。

References

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?