Tutorial

Redux Thunkを使用した非同期Reduxアクションを理解する

はじめに

デフォルトでは、Reduxのアクションは同期的にディスパッチされます。これは、外部APIと通信したり副作用を実行する必要がある重要なアプリにとっては問題です。Reduxでは、ディスパッチされるアクションとレデューサーに到達するアクションの間に位置するミドルウェアも使用できます。

副作用と非同期アクションを可能にする、非常に人気のあるミドルウェアライブラリが2つあります。Redux ThunkRedux Sagaです。この記事では、Redux Thunkについて説明します。

Thunkは、関数を使用して操作の評価/計算を遅らせるプログラミングの概念です。

Redux Thunkは、アクションオブジェクトの代わりに関数を返すアクションクリエーターを呼び出すことができるミドルウェアです。この関数はストアのディスパッチメソッドを受け取り、非同期操作が完了すると、関数本体内で通常の同期アクションをディスパッチするために使用されます。

この記事では、Redux Thunkを追加する方法と、仮のTodoアプリケーションにどのように適合するかを学びます。

前提条件

この記事は、ReactとReduxの基本的な知識があることを前提としています。Reduxを使い始めようという場合は、この記事を参照してください。

このチュートリアルは、実行すべきタスクと、完了したタスクを追跡する仮のTodoアプリケーションから構築されています。 新しいReactアプリケーションの生成にcreate-react-appが使用され、reduxreact-reduxaxiosがすでにインストールされていることを前提としています。

Todoアプリケーションを最初から構築する方法の詳細については、ここでは説明しません。これは、Redux Thunkの機能を強調するための概念設定として示されています。

redux thunkを追加する

まず、端末を使用して、プロジェクトディレクトリに移動 し、プロジェクトにredux-thunkパッケージをインストールします。

  • npm install redux-thunk@2.3.0

:Redux Thunkは、わずか14行のコードです。Reduxミドルウェアが内部でどのように機能するかについては、こちらのソースを確認してください。

ここで、ReduxのapplyMiddlewareを使用してアプリのストアを作成するときに、ミドルウェアを適用します。reduxreact-reduxを使用したReactアプリケーションを前提とすると、index.jsファイルは次のようになります。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import './index.css';
import rootReducer from './reducers';
import App from './App';
import * as serviceWorker from './serviceWorker';

// use applyMiddleware to add the thunk middleware to the store
const store = createStore(rootReducer, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

これで、Redux Thunkはアプリケーションにインポートされ、アプリケーションに適用されます。

サンプルアプリケーションでRedux Thunkを使用する

Redux Thunkの最も一般的な使用例は、外部APIと非同期で通信してデータを取得または保存することです。Redux Thunkを使用すると、リクエストのライフサイクルに従ったアクションを外部APIに簡単にディスパッチできます。

通常、新しいTodo項目を作成するには、最初にアクションをディスパッチして、Todo項目の作成が開始されたことを示します。次に、Todoアイテムが正常に作成され、外部サーバーから返された場合は、新しいTodo項目を使用して別のアクションをディスパッチします。エラーが発生し、 Todoがサーバーに保存されない場合は、代わりにエラーが発生したアクションがディスパッチされます。

Redux Thunkを使用して、これがどのように実行されるかを確認しましょう。

コンテナコンポーネントで、アクションをインポートして、ディスパッチします。

src/containers/AddTodo.js
import { connect } from 'react-redux';
import { addTodo } from '../actions';
import NewTodo from '../components/NewTodo';

const mapDispatchToProps = dispatch => {
  return {
    onAddTodo: todo => {
      dispatch(addTodo(todo));
    }
  };
};

export default connect(
  null,
  mapDispatchToProps
)(NewTodo);

このアクションは、Axiosを使用してJSONPlaceholderhttps://jsonplaceholder.typicode.com/todos)のエンドポイントにPOSTリクエストを送信します。

src/actions/index.js
import {
  ADD_TODO_SUCCESS,
  ADD_TODO_FAILURE,
  ADD_TODO_STARTED,
  DELETE_TODO
} from './types';

import axios from 'axios';

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(`https://jsonplaceholder.typicode.com/todos`, {
        title,
        userId,
        completed: false
      })
      .then(res => {
        dispatch(addTodoSuccess(res.data));
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

const addTodoSuccess = todo => ({
  type: ADD_TODO_SUCCESS,
  payload: {
    ...todo
  }
});

const addTodoStarted = () => ({
  type: ADD_TODO_STARTED
});

const addTodoFailure = error => ({
  type: ADD_TODO_FAILURE,
  payload: {
    error
  }
});

addTodoアクションクリエーターが、通常のアクションオブジェクトの代わりに関数を返す方法に注目してください。この関数は、ストアからディスパッチメソッドを受け取ります。

関数本体の内部で、最初にストアに 即時同期アクションをディスパッチして、外部APIを使用してTodoの保存を開始したことを示します。次に、Axiosを使用して、サーバーに実際のPOSTリクエストを行います。サーバーからの正常な応答の場合は、応答から受信したデータを使用して同期成功アクションをディスパッチしますが、失敗応答の場合は、エラーメッセージを使用して別の同期アクションをディスパッチします。

この場合のJSONPlaceholderのような外部APIを使用すると、実際のネットワーク遅延が発生していることが確認できます。ただし、ローカルバックエンドサーバーを使用している場合は、ネットワーク応答が速すぎて実際のユーザーが経験しているネットワーク遅延を経験できない可能性があるため、開発時に人工的な遅延を追加することができます。

src/actions/index.js
// ...

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(ENDPOINT, {
        title,
        userId,
        completed: false
      })
      .then(res => {
        setTimeout(() => {
          dispatch(addTodoSuccess(res.data));
        }, 2500);
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

// ...

エラーシナリオをテストするには、手動でエラーをスローします。

src/actions/index.js
// ...

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(ENDPOINT, {
        title,
        userId,
        completed: false
      })
      .then(res => {
        throw new Error('addToDo error!');
        // dispatch(addTodoSuccess(res.data));
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

// ...

完全を期すために、Todoレデューサーがどのようにリクエストのライフサイクル全体を処理するかを次の例で示します。

src/reducers/todosReducer.js
import {
  ADD_TODO_SUCCESS,
  ADD_TODO_FAILURE,
  ADD_TODO_STARTED,
  DELETE_TODO
} from '../actions/types';

const initialState = {
  loading: false,
  todos: [],
  error: null
};

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO_STARTED:
      return {
        ...state,
        loading: true
      };
    case ADD_TODO_SUCCESS:
      return {
        ...state,
        loading: false,
        error: null,
        todos: [...state.todos, action.payload]
      };
    case ADD_TODO_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload.error
      };
    default:
      return state;
  }
}

getStateの説明

ディスパッチメソッドを状態から受信することに加えて、Redux Thunkを使用して非同期アクションクリエーターから返される関数は、ストアのgetStateメソッドも受信するため、現在のストア値を読み取ることができます。

src/actions/index.js
export const addTodo = ({ title, userId }) => {
  return (dispatch, getState) => {
    dispatch(addTodoStarted());

    console.log('current state:', getState());

    // ...
  };
};

上記の場合、現在の状態がコンソールに出力されます。

例えば:

{loading: true, todos: Array(1), error: null}

getStateを使用すると、現在の状態に応じてさまざまな処理を行うことができます。たとえば、アプリを一度に4つのTodo項目のみに制限したい場合は、状態にTodo項目の最大数がすでに含まれている場合に、関数から戻ることができます。

src/actions/index.js
export const addTodo = ({ title, userId }) => {
  return (dispatch, getState) => {
    const { todos } = getState();

    if (todos.length > 4) return;

    dispatch(addTodoStarted());

    // ...
  };
};

上記では、アプリは4つのTodo項目に制限されます。

まとめ

このチュートリアルでは、Redux ThunkをReactアプリケーションに追加して、アクションを非同期にディスパッチできるようにする方法について見てきました。これはReduxストアを利用して、外部APIに依存している場合に役立ちます。

React について詳しく知りたい場合は、How To Code in React.js(React.js のコーディング方法) シリーズを参照するか、演習とプログラミングプロジェクトの React トピックページをご覧ください。

Creative Commons License