LoginSignup
4
7

More than 1 year has passed since last update.

Redux Toolkit(チュートリアルアプリ解説)

Last updated at Posted at 2021-07-05

はじめに

本稿ではnpx create-react-app example-app --template reduxで作成される、redux toolkitチュートリアルアプリケーションのsourceコードに、もともとコメント解説が書いてあるのですが、それをもう少しわかりやすく書き直し、追加のコメントも入れて説明しています。

コード解説しか必要ないという方はこちら

Reduxツールキットとは

ReduxツールキットはもともとReduxの次の3つの問題を対処するために作られたパッケージです。

  • Reduxストアの設定が複雑。
  • Reduxを利用するためにはたくさんのパッケージを追加する必要がある。
  • Reduxにはコピペで済むようなコードを無駄に書く必要がたくさんある。

Reduxツールキットが提供するAPI

ReduxToolkit公式ドキュメントより
(Reduxの理解を前提に書かれているのでReduxを知らない方は飛ばしてください。)

  • configureStore(): createStoreをラップして、シンプルな構成オプションと優れたデフォルトを提供します。スライスリデューサを自動的に組み合わせたり、提供するReduxミドルウェアを追加したり、デフォルトでredux-thunkを含めたり、Redux DevTools Extensionを使用できるようにしたりすることができます。
  • createReducer(): アクションタイプのルックアップテーブルをcase reducer関数に与えることができ、switch文を書く必要がありません。さらに、immerライブラリを自動的に使用して、state.todos[3].completed = trueのように、通常のミュータティブコードでよりシンプルなイミュータブルアップデートを書くことができます。
  • createAction(): 与えられたアクションタイプの文字列に対して、アクション作成関数を生成します。この関数自体にtoString()が定義されているので、型定数の代わりに使うことができます。
  • createSlice(): リデューサ関数のオブジェクト、スライス名、初期状態の値を受け取り、対応するアクション・クリエータとアクション・タイプを持つスライス・リデューサを自動的に生成する。
  • createAsyncThunk: アクション タイプの文字列と、promiseを返す関数を受け取り、その約束に基づいて保留/充足/拒否されたアクション タイプをディスパッチするサンクを生成します。
  • createEntityAdapter: ストア内の正規化されたデータを管理するために、再利用可能なリデューサとセレクタのセットを生成します。
  • ReselectライブラリのcreateSelectorユーティリティ: 使いやすさのために再移植したものです。

Reduxとは

ReduxとはReactアプリケーションの中でstateを見通しよくどこからでも扱えるようにするための仕組みことで、
以下の機能を使用して状態の取得、伝達、保持などを行います。

  • store - アプリケーションの全てのstateを保持するオブジェクト。
  • action - (削除ボタンがクリックされた)などの情報を持つオブジェクト。
  • dispatch - actionをstoreに伝達する。
  • reducer - storeから受け取ったActionとstateを元に新しいstateを計算してstoreに返します。

Reduxツールキットを使用したコード解説

npx create-react-app my-app --template redux

// または

npx create-react-app my-app --template redux-typescript

上記のコマンドでReactアプリケーションを作成すると、あらかじめRedux Toolkitのチュートリアルアプリが付属されていて、/src/features配下にcouterというフォルダが作られています。

Reduxツールキットを使用する場合ではstoreの中に、ログイン機能周りのstateとreducerを持つloginSlicer、フォロー機能周りのstateとreducerを持つfollowSlicer、いいね機能周りのstateとreducerを持つlikeSliceといった個別のかたまり(sliceと呼ぶ)を作ります。
そして、それぞれの機能を使用する際に必要なsliceを使用する流れになります。

counterフォルダ内のcomponentを見ていきます。

counterSlice.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';

const initialState = {
  value: 0, // stateの初期値
  status: 'idle', // extraReducersのstatusの初期値
};

// 以下の関数はthunkと呼ばれ、非同期のロジックを実行することができます。これは
// `dispatch(incrementAsync(10))`のように通常のアクションのようにdispatchできます。
// これは、`dispatch`関数を第一引数としてthunkを呼び出します。
// 非同期コードを実行したり、他のactionをdispatchすることができます。thunkは
// 一般的には、非同期のリクエストを行うために使用されます。
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount', // 'スライス名/メソッド名'とすることが多い。
  async (amount) => {
    const response = await fetchCount(amount);

    return response.data;
  }
);

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // reducersフィールドでは、reducerを定義し、関連するactionを生成することができます。
  reducers: {
    increment: (state) => {
      // Redux Toolkitを使うと、Reducerの中に stateを自由に変更するロジックを書くことができます。
      // ですが実際には元のstateオブジェクトを変更しません。
      // ロジック内でstateの変更を検知し、その変更に基づいて全く新しい
      // stateオブジェクトを生成します。
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // ユーザーがアクションを起こした際に受け取った値をpayloadに持っています。
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
  // createAsyncThunkを使用する際にextraReducersで
  // pending(loading時),fulfilled(成功時),rejected(失敗時),の3つのケースを指定して
  // 処理を書いていきます。
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
});
// 以下のexpotされた各actionをcomponentで使用する際、
// `onClick={() => dispatch(increment())}`のような形で
// dispatchしてstoreに出力します。
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// 以下のexportされたselectCountをcomopnentで取得する際、
// `useSelector((state: RootState) => state.counter.value)`のような形で受け取ります。
export const selectCount = (state) => state.counter.value;

export default counterSlice.reducer;

storeは/src/app/store.js のファイルにあります。
Redux Toolkitを使用する場合は、configureStoreの中にstoreが内包される形になります。

store.js
import { configureStore } from '@reduxjs/toolkit';
// このアプリケーションではsliceは一つしかありませんが、複数ある場合、全てのsliceをstore.jsにインポートします。 
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
// sliceは一つのreducerを持ち、全てのreducerをここでまとめます。
// 左側はreducerを識別するための名称になります。
    counter: counterReducer,
  },
});
Counter.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
    // useSelectorはslice内のstateを参照します。
  const count = useSelector(selectCount);
  // useDispatchはactionをsliceに伝達します。
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');

  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
                    // action.typeにdecrementという情報をcounterSliceのreducerに伝達します。
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          aria-label="Set increment amount"
          value={incrementAmount}
                    // 計算の必要のない値の一時的な保持なので、reduxを使用せずuseStateを使用している。
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          // action.typeにincrementByAmount、
          // action.paylordにincrementValueという情報を持って、
                    // counterSliceのreducerに伝達します。
          onClick={() => dispatch(incrementByAmount(incrementValue))}
        >
          Add Amount
        </button>
        <button
          className={styles.asyncButton}
          onClick={() => dispatch(incrementAsync(incrementValue))}
        >
          Add Async
        </button>
      </div>
    </div>
  );
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
// Providerで囲ってstoreを渡すことで、APP内のどこからでもstoreにアクセスすることができます。
    <Provider store={store}> 
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

serviceWorker.unregister();
counterAPI.js
// counterSlice.jsのincrementAsync()の中で呼び出される
// データの非同期リクエストを模倣したモック関数
export function fetchCount(amount = 1) {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ data: amount }), 500)
  );
}
4
7
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
4
7