【React】ReduxToolkitで非同期処理を実装する

## 概要

- Reduxを使って非同期処理を実装する際は[ReduxThunk](https://github.com/reduxjs/redux-thunk)のようなmiddlewareを使うケースが多いと思います
- しかしmiddlewareを使うと実装がややこしくなってしまうケースも多く悩ましい場面もあります
- [ReduxToolkit](https://redux-toolkit.js.org/)はReduxThunkが内包されていて実装量少なくReduxで非同期処理を作ることができます

## 事前準備

- [CreateReactApp](https://create-react-app.dev/)でReactアプリの雛形を作っておきます

```sh
npx create-react-app redux-thunk-sample
```

- 以下のコマンドで起動できます

```sh
yarn start
# open http://localhost:3000
```

## ReduxToolkitによる非同期処理の実装

- 今回はQiitaのAPIをたたいて記事一覧を取得してみます

### ライブラリの追加

- Redux関連ライブラリをインストール

```sh
yarn add react-redux @reduxjs/toolkit
```

### 通信処理の作成

- QiitaのAPIを叩く部分を作成します

```js:src/api/qiitaApi.js
export async function getItems() {
  const res = await fetch('https://qiita.com/api/v2/items');
  const json = await res.json();
  if (!res.ok) throw new Error(json.message);
  return json;
}
```

- 今回のメインとなるReduxの処理を書くファイルを作成します

```js:src/store/qiitaSlice.js
import { createSlice } from '@reduxjs/toolkit';
import { getItems } from '../api/qiitaApi';

// Slice
export const qiitaSlice = createSlice({
  name: 'qiita',
  // stateの初期値を設定
  initialState: { loading: false, error: null, items: [] },
  reducers: {
    // 通信を開始した時に呼ぶ関数
    fetchStart(state, action) {
      state.loading = true;
      state.error = null;
    },
    // 通信が失敗した時に呼ぶ関数
    fetchFailure(state, action) {
      state.loading = false;
      state.error = action.payload;
    },
    // 通信が成功した時に呼ぶ関数
    fetchSuccess(state, action) {
      state.loading = false;
      state.error = null;
      state.items = action.payload;
    },
  },
});

// Actions
export const { fetchStart, fetchFailure, fetchSuccess } = qiitaSlice.actions;

// 外部からはこの関数を呼んでもらう
export const fetchItems = () => async dispatch => {
  try {
    dispatch(fetchStart());
    dispatch(fetchSuccess(await getItems()));
  } catch (error) {
    dispatch(fetchFailure(error.stack));
  }
};

// Selectors
export const selectQiita = ({ qiita }) => qiita;

// Reducer(must be default export)
export default qiitaSlice.reducer;
```

- 通信中かどうかを示す`loading`、エラー情報を入れる`error`、Qiitaから取得した記事を入れる`items`の3つのStateを持ちます
- 非同期ではない場合は`reducers`に定義した関数を外部からdispatchしていました
    - 非同期処理の場合は処理の開始時と終了時にStateを更新したくなります
    - なので外部から直接`reducers`に定義した関数をdispatchせずにワンクッション挟む関数(ここではfetchItems)をdispatchしてもらうことで対応できます

### 記事一覧を表示するコンポーネントの作成

- Reduxの処理を呼び出して取得した情報を表示するコンポーネントを作成します

```jsx:src/components/QiitaItems.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectQiita, fetchItems } from '../store/qiitaSlice';

function QiitaItems() {
  const dispatch = useDispatch();
  const { loading, error, items } = useSelector(selectQiita);

  useEffect(() => {
    // fetchItemsを実行
    dispatch(fetchItems());
  }, [dispatch]);

  if (loading) return <p>...loading</p>;
  if (error) return <p>{error}</p>;

  return items.map(item => (
    <div key={item.id}>
      <h2><a href={item.url}>{item.title}</a></h2>
      <p>{item.created_at} by {item.user.id}</p>
    </div>
  ));
}

export default QiitaItems;
```

- useEffectで`fetchItems`をdispatchしているのでコンポーネント生成時に通信処理を呼び出している感じです
- 通信中は`...loading`と表示されて完了すると記事の情報が表示されるようにしています

### Reduxのセッティング

- Reduxを使うための設定を入れておきます
- App.jsの修正

```jsx:src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import QiitaItems from './components/QiitaItems';

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

export default App;
```

- Storeの作成

```js:src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import qiitaReducer from './qiitaSlice';

export const store = configureStore({
  reducer: {
    qiita: qiitaReducer,
  },
});
```

## 動作確認

- うまくいっていれば以下のようになっているはずです

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/2f66b100-a81b-154e-1e8a-3d7478e8618a.gif)

## まとめ

- ReduxToolkitを使うと通信処理もそれほど複雑さを感じずに実装できました
- 実装されたコードもわかりやすくていいですね