Redux Toolkit サクッと入門


# はじめに

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


# 本記事の流れ

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

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

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

- Vite: 5.0.8
- React: 18.2.0
- TypeScript: 5.2.2

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

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

では、本題に入ります。

# ストアの作成

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

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

```typescript
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 記法と比較してみます。

```typescript
// 従来の 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](https://immerjs.github.io/immer/) が使用されていることにより、Redux State の不変性を意識した更新処理が不要になったからです。

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

```typescript
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 をまとめた型となります。

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

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

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

```tsx
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` のインターフェースの定義は以下です。
```typescript
// Interface
<TState = unknown, Selected = unknown>(selector: (state: TState) => Selected, equalityFn?: EqualityFn<Selected>): Selected;
```

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

```typescript
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` フックを使います。  

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

const dispatch = useDispatch<AppDispatch>();
```

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

```tsx
// 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));` の部分でアクションを発行しています。アクションは以下の形式で発行されます。

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

# API 通信を伴う State の更新

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

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

```typescript
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` 起点のアクションは以下になります。

```json
{
  "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` に追加します。

```typescript
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 の内容に応じて画面表示を切り替えてみます。

```tsx
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
- [Redux Toolkit](https://redux-toolkit.js.org/)
- [Redux](https://redux.js.org/)