Reduxで非同期処理を実装する方法は主にRedux Thunk、Redux Saga、Redux Observableがあります。
Redux ThunkはRedux Toolkitにデフォルトでインストールされているミドルウェアです。そのため、手軽にReduxで非同期処理を実装したい場合はRedux Thunkを利用するケースが多いでしょう。
今回はAPIを利用してTodoリストを取得するTodoアプリを例に、Redux Thunkによる非同期処理の実装方法について紹介をします。
reduxjs/toolkit
は1.5.0、react-redux
は7.1.16、redux-thunk
は2.3.0、react
は17.0.2、typescript
は3.8.3を利用しています。
目次
今回利用するAPIサーバについて
APIサーバはJSON Serverを利用することで簡単に作成できます。
今回作成するTodoアプリは、4000番ポートにアクセスするとTodoリストが取得できる前提で実装をします。
実際にTodoアプリの挙動を確認する際は以下の手順でAPIサーバを用意してください。
todos.json
{
"todos": [
{
"id": 1,
"content": "do something",
"completed": false
},
{
"id": 2,
"content": "go somewhere",
"completed": false
}
]
}
### json-serverのインストール
$ yarn add json-server
### port4000でJSON Serverを起動し、todos.jsonが取得できるようにする
$ json-server todos.json --port 4000
モックを作成する
create-react-appを利用してアプリケーションのひな型を作成します。
### create-react-appを利用してアプリケーションを作成
$ npx create-react-app redux-thunk-todoapp --template redux-typescript
$ cd redux-thunk-todoapp
### アプリケーションで必要になるnpmのインストール
$ yarn add classnames @types/classnames
### 今回利用するHTTPクライアントであるaxiosをインストール
$ yarn add axios
### srcディレクトリのファイルを削除
$ cd redux-thunk-todoapp
$ rm -rf src/*
モック完成時のディレクトリ構成は以下のようになります。
./src
├── TodoApp.tsx
├── components
│ ├── Todo.tsx
│ └── TodoList.tsx
├── index.tsx
├── redux
│ ├── reducers
│ │ ├── index.ts
│ │ └── todos.ts
│ ├── store.ts
│ └── types.ts
└── styles.css
モックのソースコードはnishina555/redux-thunk-todoappのこちらのコミットでも公開をしています。
Component
モックのUIとなるコンポーネントを作成します。
src/TodoApp.tsx
import React from "react";
import TodoList from "./components/TodoList";
import "./styles.css";
export default function TodoApp() {
return (
<div className="todo-app">
<h1>Todo List</h1>
<TodoList />
</div>
);
}
src/components/TodoList.tsx
import React from "react";
import Todo from "./Todo";
const TodoList = () => {
const todos: any = {};
return (
<ul className="todo-list">
{todos && todos.length
? todos.map((todo: any, index: any) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
};
export default TodoList;
src/components/Todo.tsx
import React from "react";
import cx from "classnames";
import { TodoItem } from "../redux/types";
type TodoProps = {
todo: TodoItem;
};
const Todo: React.FC<TodoProps> = ({ todo }) => (
<li
className="todo-item"
onClick={() => {} /** dispatches action to toggle todo */}
>
{todo && todo.completed ? "👌" : "👋"}{" "}
<span
className={cx(
"todo-item__text",
todo && todo.completed && "todo-item__text--completed"
)}
>
{todo.content}
</span>
</li>
);
export default Todo;
Reducer
Todoに関するReducerのひな型を作成します。
この段階ではActionは作成せず、Reducerは現在のStateをそのままreturnするだけの実装となっています。
src/redux/reducers/index.ts
import { combineReducers } from "redux";
import todos from "./todos";
export default combineReducers({ todos });
src/redux/reducers/todos.ts
import { TodoState } from "../types";
const initialState: TodoState = {
todoItems: [],
};
const todos = (state = initialState, action: any): any => {
return state;
};
export default todos;
Store
作成したReducerをStoreに読み込ませます。
今回はRedux ToolkitのconfigureStore
を利用してStoreを作成しました。
src/redux/store.ts
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const store = configureStore({
reducer: rootReducer,
});
export default store;
Provider
Storeのつなぎこみをします。
src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import TodoApp from "./TodoApp";
import { Provider } from "react-redux";
import store from "./redux/store";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<TodoApp />
</Provider>,
rootElement
);
そのほか
必要になるtypeを定義します。
src/redux/types.ts
export type TodoItem = {
id: number;
content: string;
completed: boolean;
}
export type TodoState = {
todoItems: Array<TodoItem>;
}
export type RootState = {
todos: TodoState;
}
CSSは以下の通りです。
src/styles.css
.todo-app {
font-family: sans-serif;
}
/** add todo **/
.add-todo {
margin-left: 0.5rem;
}
/** todo list **/
.todo-list {
margin-top: 1rem;
text-align: left;
list-style: none;
}
/** todo item **/
.todo-item {
font-family: monospace;
cursor: pointer;
line-height: 1.5;
}
.todo-item__text--completed {
text-decoration: line-through;
color: lightgray;
}
/** visibility filters **/
.filter {
padding: 0.3rem 0;
margin: 0 0.3rem;
cursor: pointer;
}
.filter--active {
border-bottom: 1px solid black;
}
動作確認
yarn start
で起動し、以下のような画面が表示されればOKです。

同期処理でTodoを表示させる
非同期処理やRedux Thunkのことはいったん置いておいて、まずは同期処理でTodoリストを画面に表示する実装をします。
読み込んだTodoリストをStateにセットするActionを作成します。
src/redux/actions.ts
import { ActionTypes } from "./actionTypes";
import { TodoItem } from "./types";
type SetTodosAction = {
type: ActionTypes.SET_TODOS;
payload: { todos: TodoItem[] };
};
export const setTodos = (todos: TodoItem[]): SetTodosAction => ({
type: ActionTypes.SET_TODOS,
payload: { todos },
});
export type TodoActions = SetTodosAction;
src/redux/actionTypes.ts
export enum ActionTypes {
SET_TODOS = "SET_TODOS",
}
作成したActionに対応するReducerを実装します。
src/redux/reducers/todos.ts
import { ActionTypes } from "../actionTypes";
import { TodoState } from "../types";
import { TodoActions } from "../actions";
const initialState: TodoState = {
todoItems: [],
};
const todos = (state = initialState, action: TodoActions): TodoState => {
switch (action.type) {
case ActionTypes.SET_TODOS: {
const { todos } = action.payload;
return {
todoItems: todos,
};
}
default:
return state;
}
};
export default todos;
TodoList
コンポーネントがマウントされたタイミングでTodoリストを取得・表示するようにします。
マウント時に実行されるuseEffect()
内でTodoリストの読み込み、さきほど作成したActionを実行(dispatch)します。
src/components/TodoList.tsx
import React, { useEffect } from "react";
import Todo from "./Todo";
import { setTodos, TodoActions } from "../redux/actions";
import { RootState } from "../redux/types";
import { connect } from "react-redux";
import { TodoItem } from "../redux/types";
type TodoListProps = {
todos: Array<TodoItem>;
setTodos: (todos: Array<TodoItem>) => TodoActions;
};
const TodoList: React.FC<TodoListProps> = ({ todos, setTodos }) => {
useEffect(() => {
const data: Array<TodoItem> = [
{ id: 1, content: "do something", completed: false },
{ id: 2, content: "go somewhere", completed: false },
];
setTodos(data);
}, []); // 第2引数を空配列にすることでマウント・アンマウント時のみ実行される
return (
<ul className="todo-list">
{todos && todos.length
? todos.map((todo: any, index: any) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
};
const mapStateToProps = (state: RootState) => {
const todos = state.todos.todoItems;
return { todos };
};
export default connect(mapStateToProps, { setTodos })(TodoList);
APIサーバとTodoアプリを起動し、以下のような画面が表示されればOKです。

非同期処理でTodoを表示させる(Redux Thunkなし)
Redux Thunkの導入の前に、まずはRedux Thunkなしで非同期処理を実装したTodoアプリの紹介をします。
マウント時に実行されるuseEffect()
内でAPIにアクセスをし、レスポンスデータをsetTodos
アクションを利用してStateにセットします。
src/components/TodoList.tsx
import React, { useEffect } from "react";
import Todo from "./Todo";
import { setTodos, TodoActions } from "../redux/actions";
import { RootState } from "../redux/types";
import { connect } from "react-redux";
import { TodoItem } from "../redux/types";
import axios from "axios";
type TodoListProps = {
todos: Array<TodoItem>;
setTodos: (todos: Array<TodoItem>) => TodoActions;
};
const TodoList: React.FC<TodoListProps> = ({ todos, setTodos }) => {
useEffect(() => {
const fetchTodos = async () => {
const response = await axios
.get(`http://localhost:4000/todos`)
.catch((error) => {
throw new Error(error.message);
});
setTodos(response.data);
};
fetchTodos();
}, []);
return (
<ul className="todo-list">
{todos && todos.length
? todos.map((todo: TodoItem, index: number) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
};
const mapStateToProps = (state: RootState) => {
const todos = state.todos.todoItems;
return { todos };
};
export default connect(mapStateToProps, { setTodos })(TodoList);
async/awaitの概要やエラーハンドリング方法についてはJavaScriptの非同期処理の定番!async/await概要・挙動まとめで解説をしています。
非同期処理でTodoを表示させる(Redux Thunkあり)
Redux Thunkを利用した非同期処理の方法について紹介します。
そもそもThunkとは?Redux Thunkについて
先ほど紹介したRedux Thunkを利用しない非同期処理の実装は『コンポーネントで非同期処理 → Actionのdispatch』という手順をふみました。
非同期処理がコンポーネントに実装されているとコンポーネントのロジックが複雑になるので、Actionで非同期処理を行いたいと考えたとします。
しかしReduxでは、dispatchするActionはtypeとpalyoadを持つオブジェクトでなければいけません。
そのため、async関数(非同期処理)でActionCreatorを実装した場合、ActionオブジェクトではなくPromiseオブジェクトが返されてしまうため、Actionに非同期処理を実装できません。
そこで登場するのがRedux Thunkです。
Thunkとは『関数を使用して式の評価や計算を遅らせるプログラミングの概念』です。
たとえば、以下のようなものがThunk関数です。
let thunk = () => 1 + 2
// この時点では 1 + 2 は評価されない
thunk()
// 3
// Thunk関数が呼び出されたタイミングで 1 + 2 が評価される
Redux ThunkはThunkの概念をReduxで利用できるようにするミドルウェアです。
Redux Thunkを導入することで、dispatch()
にActionオブジェクトだけでなく、非同期処理が実装されている関数も渡すことができます。
Actionがdispatchされた場合は通常通りReducerが実行されます。関数がdispatchされた場合はRedux Thunkで処理が行われ、Redux ThunkからActionのdispatchが実行されます。
Reduxの非同期処理のデータフローはRedux『Redux Async Data Flow』で紹介されている図がわかりやすいです。
Redux Thunkのインストール
Redux Thunkを有効にするためStoreを編集します。
Redux ToolkitのAPI(configureStore
)の場合、デフォルトでRedux ThunkとRedux DevTools Extensionが有効になっているため、ここで紹介するRedux Thunkのインストール作業は不要です。
以下はconfigureStore
を利用している場合の手順となります。
redux-thunk
をインストールし、applyMiddleware
を読み込むことでRedux Thunkが利用できます。1
$ yarn add redux-thunk
src/redux/store.ts
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
+ applyMiddleware(thunk)
);
export default store
Redux Thunkに加え、Redux DevTools Extensionも利用する場合はredux-devtools-extension
のcomposeWithDevTools
を利用して以下のようにします。2
src/redux/store.ts
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(
rootReducer,
+ composeWithDevTools(applyMiddleware(thunk))
);
export default store
ThunkActionを返すActionCreatorを実装する
ThunkActionは『DispatchとStateを引数にもつ関数』です。
つまりThunkActionを返すActionCreatorとは、関数を返すActionCreatorのことを指します。
ThunkAction内に非同期処理を実装することで、Reduxでも非同期処理が扱えます。
ActionのdispatchはThunkAction内で行います。
ThunkActionの型定義はThunkAction<R, S, E, A>
となっており、それぞれの意味は以下の通りです。
- R(第一引数): Actionの戻り値の型
- S(第二引数): Stateの型
- E(第三引数): ThunkActionのextraArgument(dispatch・getState以外の引数)の型
- A(第四引数): Actionの型
『APIから非同期で取得したデータをStateに保存する』という処理(fetchTodos()
)はThunkActionを利用すると以下のようにかけます。
fetchTodos()
はThunkActionという関数を返し、ThunkAction内で非同期処理が行われます。
src/redux/actions.ts
import { ActionTypes } from "./actionTypes";
import { TodoItem, RootState } from "./types";
import { Dispatch } from "redux";
import axios from "axios";
import { ThunkAction } from "redux-thunk";
type SetTodosAction = {
type: ActionTypes.SET_TODOS;
payload: { todos: TodoItem[] };
};
export const setTodos = (todos: TodoItem[]): SetTodosAction => ({
type: ActionTypes.SET_TODOS,
payload: { todos },
});
export const fetchTodos = (): ThunkAction<
void,
RootState,
unknown,
TodoActions
> => {
return async (dispatch: Dispatch<TodoActions>) => {
const response = await axios
.get(`http://localhost:4000/todos`)
.catch((error) => {
throw new Error(error.message);
});
dispatch(setTodos(response.data));
};
};
export type TodoActions = SetTodosAction;
{ return (...) }
の部分は省略可能ですので、以下のようにアロー関数を2つ並べた形で記述もできます。
src/redux/actions.ts
export const fetchTodos = (): ThunkAction<
void,
RootState,
unknown,
TodoActions
> => async (dispatch: Dispatch<TodoActions>) => {
const response = await axios
.get(`http://localhost:4000/todos`)
.catch((error) => {
throw new Error(error.message);
});
dispatch(setTodos(response.data));
};
ThunkActionの型定義を明記せず、以下のようにシンプルな形で記述もできます。
src/redux/actions.ts
export const fetchTodos = () => async (dispatch: Dispatch<TodoActions>) => {
const response = await axios
.get(`http://localhost:4000/todos`)
.catch((error) => {
throw new Error(error.message);
});
dispatch(setTodos(response.data));
};
もしgetState()
を利用する場合はたとえば以下のように第二引数で指定します。
// getStateを利用するThunkAction部分のサンプル
async (dispatch , getState) => {
const { todos } = getState(); // 現在のStateのtodosが取得できる
dispatch(addTodo(todo.content, todos.todoItems.length));
}
ThunkActionをコンポーネントから呼び出す
さきほど作成したfetchTodos()
の呼び出しは以下のようになります。
src/components/TodoList.tsx
import React, { useEffect } from "react";
import Todo from "./Todo";
import { fetchTodos } from "../redux/actions";
import { RootState } from "../redux/types";
import { connect } from "react-redux";
import { TodoItem } from "../redux/types";
type TodoListProps = {
todos: Array<TodoItem>;
fetchTodos: () => void;
};
const TodoList: React.FC<TodoListProps> = ({ todos, fetchTodos }) => {
useEffect(() => {
fetchTodos();
}, []);
return (
<ul className="todo-list">
{todos && todos.length
? todos.map((todo: TodoItem, index: number) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
};
const mapStateToProps = (state: RootState) => {
const todos = state.todos.todoItems;
return { todos };
};
export default connect(mapStateToProps, { fetchTodos })(TodoList);
Todoアプリを起動し、Todoの一覧が確認できたらOKです。

参考: APIアクセスに関する専用クラスを作成する
Redux Thunkを利用した場合、Actionのソースコード内でawait const response axios.get(...)
と記述をすることで非同期通信ができます。
上記の方法でも問題はないのですが、APIアクセスに関するロジックは専用クラスに外だしすることでActionの実装が簡潔になります。
リファクタリングの一例を紹介します。
src/api/index
import axios from "axios";
const baseURL = `http://localhost:4000/`;
const axiosInstance = axios.create({
baseURL: baseURL,
});
export default axiosInstance;
src/api/todos.ts
import axiosInstance from "./index";
class Todos {
static async getAll() {
const response = await axiosInstance.get(`todos`).catch((error) => {
throw new Error(error.message);
});
return response.data;
}
}
export default Todos;
上記のクラスを利用した場合、APIアクセスは以下のようになります。
// Before: 非同期通信をActionで実装する場合
const response = await axios
.get(`http://localhost:4000/todos`)
.catch((error) => {
throw new Error(error.message);
});
dispatch(setTodos(response.data));
// After: APIアクセスのクラスを利用する場合
const todos = await TodosApiService.getAll();
dispatch(setTodos(todos));
まとめ
以上でRedux Thunkを利用した非同期処理の実装方法の紹介を終わります。
- Thunkとは『関数を使用して式の評価や計算を遅らせるプログラミングの概念』のこと
- Redux Thunkを導入することでReduxで非同期処理を実装できる
- ThunkActionを返すActionCreatorを実装し、ThunkAction内で非同期処理を実装する
本記事ではTodoリストを表示させる部分までしか実装していませんが、『Todoリストのフィルタリング』と『Todoのステータス変更』も実装したソースコードをnishina555/redux-thunk-todoappに公開しました。以下のような画面のTodoアプリとなっています。
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!