メインコンテンツへスキップ

Redux 基礎 Part 6: 非同期ロジックとデータ取得

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

学ぶこと
  • 非同期データにおける Redux データフローの仕組み
  • 非同期ロジックのための Redux ミドルウェアの使用方法
  • 非同期リクエスト状態を扱うパターン
前提知識
  • サーバーからデータを取得/更新する HTTP リクエストの使用経験
  • Promise を含む JavaScript の非同期ロジックの理解

はじめに

Part 5: UI と Reactでは、React コンポーネントが Redux ストアとやり取りするために React-Redux ライブラリを使用する方法を見てきました。これには Redux 状態を読み取る useSelector の呼び出し、dispatch 関数へのアクセスを提供する useDispatch の呼び出し、そしてアプリを <Provider> コンポーネントでラップしてこれらのフックにストアへのアクセスを許可することが含まれます。

これまで扱ってきたデータはすべて React+Redux クライアントアプリケーション内に直接存在していました。しかし実際のアプリケーションのほとんどは、HTTP API 呼び出しによってアイテムを取得・保存するサーバーデータを扱う必要があります。

このセクションでは、TODO アプリを更新して API から TODO を取得し、新しい TODO を API に保存して追加します。

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

注意

このチュートリアルでは、Reduxの基本原則と概念を説明するために、あえて旧来のスタイルのReduxロジックパターンを使用しています。これらは、現代的なReduxアプリ開発の正しいアプローチとして推奨しているRedux Toolkitを使った「モダンRedux」パターンに比べてコード量が多くなります。このチュートリアルは_プロダクション環境で使用することを想定したものではありません_。

「モダンRedux」をRedux Toolkitで実践する方法については、以下のページを参照してください:

ヒント

Redux Toolkit には RTK Query データ取得・キャッシュ API が含まれています。RTK Query は Redux アプリ向けに設計されたデータ取得・キャッシュソリューションであり、データ取得管理のための thunk や reducer を一切書かずに済むようになります。私たちはデータ取得のデフォルトアプローチとして RTK Query を特に推奨しており、RTK Query はこのページで示すものと同じパターンに基づいて構築されています。

RTK Query を使ったデータ取得方法は Redux エッセンシャル Part 7: RTK Query 基礎 で学べます。

REST APIとクライアントの例

例題プロジェクトを独立させつつ現実的なものにするため、初期プロジェクト設定にはデータ用の偽のインメモリ REST API が既に含まれています(Mirage.js モック API ツールを使用して構成)。この API はエンドポイントのベース URL として /fakeApi を使用し、/fakeApi/todos に対して一般的な GET/POST/PUT/DELETE HTTP メソッドをサポートします。これは src/api/server.js で定義されています。

プロジェクトにはまた、axios のような人気のある HTTP ライブラリと同様に client.get()client.post() メソッドを公開する小さな HTTP API クライアントオブジェクトが含まれています。これは src/api/client.js で定義されています。

このセクションでは、インメモリの偽のREST APIに対してHTTP呼び出しを行うためにclientオブジェクトを使用します。

Redux ミドルウェアと副作用

Reduxストア自体は非同期ロジックについて何も知りません。同期的にアクションをディスパッチし、ルートリデューサー関数を呼び出して状態を更新し、何かが変更されたことをUIに通知することしかできません。非同期処理はストアの外部で行われる必要があります。

以前、Redux reducer は決して「副作用」を含んではならないと説明しました。「副作用」とは、関数から値を返すこと以外で観測可能な状態や動作の変更のことです。一般的な副作用の種類には以下があります:

  • コンソールへの値の出力

  • ファイルの保存

  • 非同期タイマーの設定

  • HTTP リクエストの実行

  • 関数外部の状態変更や引数の変更

  • 乱数や一意な ID の生成(例: Math.random()Date.now()

しかし実際のアプリケーションでは、こうした処理をどこかで行う必要があります。では reducer に副作用を置けない場合、どこに置けるのでしょうか?

Redux ミドルウェアは副作用を持つロジックを記述できるように設計されています

パート4で説明した通り、Reduxミドルウェアはディスパッチされたアクションに対してあらゆる処理を実行できます:ログ記録、アクションの変更、遅延実行、非同期呼び出しなど。またミドルウェアは実際のstore.dispatch関数をパイプライン化するため、プレーンなアクションオブジェクト以外の値をdispatchに渡すことも可能です。ミドルウェアがその値をインターセプトし、リデューサーに到達させなければよいのです。

ミドルウェアはdispatchgetStateにもアクセスできます。つまり非同期ロジックをミドルウェア内に記述しつつ、アクションをディスパッチしてReduxストアとやり取りすることが可能です。

非同期ロジックを有効化するミドルウェアの使用

ミドルウェアがReduxストアと連携する非同期ロジックをどのように実現するか、いくつかの例を見てみましょう。

特定のアクションタイプを検出し、非同期ロジックを実行するミドルウェアを作成する方法があります:

import { client } from '../api/client'

const delayedActionMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
// Delay this action by one second
next(action)
}, 1000)
return
}

return next(action)
}

const fetchTodosMiddleware = storeAPI => next => action => {
if (action.type === 'todos/fetchTodos') {
// Make an API call to fetch todos from the server
client.get('todos').then(todos => {
// Dispatch an action with the todos we received
storeAPI.dispatch({ type: 'todos/todosLoaded', payload: todos })
})
}

return next(action)
}
情報

Reduxが非同期ロジックにミドルウェアを使用する理由と方法の詳細は、Redux作者Dan AbramovのStackOverflow回答を参照してください:

非同期関数ミドルウェアの作成

前節のミドルウェアは特定の処理に特化していました。ミドルウェア自体とは切り離して任意の非同期ロジックを事前に記述し、dispatchgetStateでストアとやり取りできる方法があると便利です。

dispatchにアクションオブジェクトではなく関数を渡せるミドルウェアを作ったらどうでしょうか?ミドルウェアが「アクション」が実際に関数かどうかをチェックし、関数なら即座に呼び出します。これにより非同期ロジックをミドルウェア定義の外側の別関数に記述できます。

このミドルウェアの実装例:

Example async function middleware
const asyncFunctionMiddleware = storeAPI => next => action => {
// If the "action" is actually a function instead...
if (typeof action === 'function') {
// then call the function and pass `dispatch` and `getState` as arguments
return action(storeAPI.dispatch, storeAPI.getState)
}

// Otherwise, it's a normal action - send it onwards
return next(action)
}

そして、このミドルウェアを次のように使用できます:

const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)

// Write a function that has `dispatch` and `getState` as arguments
const fetchSomeData = (dispatch, getState) => {
// Make an async HTTP request
client.get('todos').then(todos => {
// Dispatch an action with the todos we received
dispatch({ type: 'todos/todosLoaded', payload: todos })
// Check the updated store state after dispatching
const allTodos = getState().todos
console.log('Number of todos after loading: ', allTodos.length)
})
}

// Pass the _function_ we wrote to `dispatch`
store.dispatch(fetchSomeData)
// logs: 'Number of todos after loading: ###'

繰り返しますが、この「非同期関数ミドルウェア」はdispatchに関数を渡せるようにします! 関数内部では非同期ロジック(HTTPリクエスト)を記述し、リクエスト完了時に通常のアクションオブジェクトをディスパッチできます。

Reduxの非同期データフロー

ミドルウェアと非同期ロジックはReduxアプリ全体のデータフローにどのような影響を与えるのでしょうか?

通常のアクションと同様に、まずボタンクリックなどのユーザーイベントを処理します。次にdispatch()を呼び出し、プレーンなアクションオブジェクト、関数、またはミドルウェアが検出できる任意の値を渡します。

ディスパッチされた値がミドルウェアに到達すると、非同期呼び出しを実行し、非同期処理完了時に実際のアクションオブジェクトをディスパッチできます。

以前に通常の同期型Reduxデータフローを表す図を見ました。Reduxアプリに非同期ロジックを追加すると、ミドルウェアがHTTPリクエストなどのロジックを実行してからアクションをディスパッチする追加ステップが入ります。これにより非同期データフローは次のようになります:

Redux非同期データフロー図

Redux Thunkミドルウェアの使用

実はReduxには公式版の「非同期関数ミドルウェア」が存在し、Redux "Thunk"ミドルウェアと呼ばれています。ThunkミドルウェアはdispatchgetStateを引数として受け取る関数の記述を可能にします。Thunk関数内部には任意の非同期ロジックを記述でき、そのロジックは必要に応じてアクションをディスパッチしたりストア状態を読み取ったりできます。

非同期ロジックをサンク関数として記述することで、使用するReduxストアを事前に知らなくてもそのロジックを再利用できます

情報

「サンク」という用語は、プログラミング用語で「遅延処理を行うコード片」を意味します。サンクの使用方法の詳細については、以下のガイドページを参照してください:

以下の記事も参考になります:

ストアの設定

Reduxのサンクミドルウェアはredux-thunkという名前のパッケージとしてNPMで提供されています。アプリで使用するにはこのパッケージをインストールする必要があります:

npm install redux-thunk

インストール後、TodoアプリのReduxストアを更新してこのミドルウェアを使用できます:

src/store.js
import { createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunk))

// The store now has the ability to accept thunk functions in `dispatch`
const store = createStore(rootReducer, composedEnhancer)
export default store

サーバーからTodoを取得する

現在、Todoエントリはクライアントのブラウザ内にしか存在しません。アプリ起動時にサーバーからTodoリストを読み込む方法が必要です。

まず/fakeApi/todosエンドポイントに対してHTTP呼び出しを行い、Todoオブジェクトの配列をリクエストするサンク関数を作成します。その後、その配列をペイロードに含むアクションをディスパッチします。これはTodo機能全般に関連するため、todosSlice.jsファイルにサンク関数を記述します:

src/features/todos/todosSlice.js
import { client } from '../../api/client'

const initialState = []

export default function todosReducer(state = initialState, action) {
// omit reducer logic
}

// Thunk function
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}

このAPI呼び出しはアプリケーションの初回読み込み時に1度だけ実行したいものです。配置可能な場所はいくつかあります:

  • <App>コンポーネントのuseEffectフック内

  • <TodoList>コンポーネントのuseEffectフック内

  • index.jsファイル内(ストアのインポート直後)

まずはindex.jsに直接配置してみましょう:

src/index.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import './index.css'
import App from './App'

import './api/server'

import store from './store'
import { fetchTodos } from './features/todos/todosSlice'

store.dispatch(fetchTodos)

const root = createRoot(document.getElementById('root'))

root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)

ページを再読み込みしてもUIに変化は見られません。しかしRedux DevTools拡張機能を開くと、'todos/todosLoaded'アクションがディスパッチされており、偽装サーバーAPIが生成したTodoオブジェクトが含まれているはずです:

DevTools - todosLoadedアクションの内容

アクションをディスパッチしたにもかかわらず状態が変化していない点に注意してください。状態を更新するにはTodoリデューサーでこのアクションを処理する必要があります。

リデューサーにケースを追加してデータをストアに読み込みましょう。サーバーから取得したデータで既存のTodoを完全に置き換えるため、action.payload配列を返して新しいTodoのstate値とします:

src/features/todos/todosSlice.js
import { client } from '../../api/client'

const initialState = []

export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other reducer cases
case 'todos/todosLoaded': {
// Replace the existing state entirely by returning the new value
return action.payload
}
default:
return state
}
}

export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}

アクションのディスパッチは即座にストアを更新するため、サンク内でgetStateを呼び出してディスパッチ後の更新状態を読み取れます。例えば'todos/todosLoaded'アクションのディスパッチ前後でTodoの総数をコンソールに出力できます:

export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')

const stateBefore = getState()
console.log('Todos before dispatch: ', stateBefore.todos.length)

dispatch({ type: 'todos/todosLoaded', payload: response.todos })

const stateAfter = getState()
console.log('Todos after dispatch: ', stateAfter.todos.length)
}

Todoアイテムの保存

新しいTodoアイテム作成時にはサーバーも更新する必要があります。'todos/todoAdded'アクションを即時ディスパッチする代わりに、初期データでサーバーにAPIリクエストを送信し、新規保存されたTodoアイテムのコピーが返されるのを待ってから、そのTodoアイテムを含むアクションをディスパッチします。

しかしこのロジックをサンク関数として記述しようとすると問題が発生します。todosSlice.jsファイルでサンクを別関数として記述しているため、API呼び出しを行うコードは新しいTodoのテキスト内容を知らないのです:

src/features/todos/todosSlice.js
async function saveNewTodo(dispatch, getState) {
// ❌ We need to have the text of the new todo, but where is it coming from?
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
dispatch({ type: 'todos/todoAdded', payload: response.todo })
}

textをパラメータとして受け取る関数を作成する必要がありますが、実際にはそのtext値を使ってAPI呼び出しを行うthunk関数を生成します。外側の関数はthunk関数を返すように実装し、コンポーネント内でdispatchに渡せるようにします。

src/features/todos/todosSlice.js
// Write a synchronous outer function that receives the `text` parameter:
export function saveNewTodo(text) {
// And then creates and returns the async thunk function:
return async function saveNewTodoThunk(dispatch, getState) {
// ✅ Now we can use the text value and send it to the server
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
dispatch({ type: 'todos/todoAdded', payload: response.todo })
}
}

これで<Header>コンポーネントで使用できます:

src/features/header/Header.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'

import { saveNewTodo } from '../todos/todosSlice'

const Header = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()

const handleChange = e => setText(e.target.value)

const handleKeyDown = e => {
// If the user pressed the Enter key:
const trimmedText = text.trim()
if (e.which === 13 && trimmedText) {
// Create the thunk function with the text the user wrote
const saveNewTodoThunk = saveNewTodo(trimmedText)
// Then dispatch the thunk function itself
dispatch(saveNewTodoThunk)
setText('')
}
}

// omit rendering output
}

thunk関数をコンポーネント内で即座にdispatchに渡すことが分かっているため、一時変数の作成を省略できます。代わりにsaveNewTodo(text)を呼び出し、生成されたthunk関数を直接dispatchに渡せます:

src/features/header/Header.js
const handleKeyDown = e => {
// If the user pressed the Enter key:
const trimmedText = text.trim()
if (e.which === 13 && trimmedText) {
// Create the thunk function and immediately dispatch it
dispatch(saveNewTodo(trimmedText))
setText('')
}
}

これによりコンポーネントはthunk関数をディスパッチしていること自体を認識しなくなりました。saveNewTodo関数が実際の処理をカプセル化しています。<Header>コンポーネントは、ユーザーがEnterキーを押した時に_何らかの値_をディスパッチする必要があることだけを知っています。

dispatchに渡す内容を準備する関数を書くこのパターンは**「アクションクリエーター」パターン**と呼ばれ、次のセクションで詳しく説明します。

更新された'todos/todoAdded'アクションがディスパッチされているのを確認できます:

Devtools - 非同期todoAddedアクションの内容

最後に変更が必要なのはtodosリデューサーの更新です。/fakeApi/todosへのPOSTリクエストを行うと、サーバーは(新しいID値を含む)完全な新しいtodoオブジェクトを返します。つまりリデューサーは新しいIDを計算したり他のフィールドを埋めたりする必要がなく、新しいtodoアイテムを含むstate配列を作成するだけで済みます:

src/features/todos/todosSlice.js
const initialState = []

export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Return a new todos state array with the new todo item at the end
return [...state, action.payload]
}
// omit other cases
default:
return state
}
}

これで新しいtodoの追加が正しく機能します:

Devtools - 非同期todoAddedの状態差分

ヒント

Thunk関数は非同期ロジックと同期ロジックの両方に使用できます。ThunkはdispatchgetStateへのアクセスが必要な再利用可能なロジックを記述する方法を提供します。

学んだこと

これでtodoアプリの更新が完了し、偽のサーバーAPIへのHTTPリクエストに「thunk」関数を使用してtodoアイテムのリストを取得し、新しいtodoアイテムを保存できるようになりました。

この過程で、Reduxミドルウェアを使用して非同期呼び出しを行い、非同期呼び出し完了後にアクションをディスパッチしてストアと連携する方法を確認しました。

現在のアプリの外観は次のとおりです:

まとめ
  • Reduxミドルウェアは副作用のあるロジックを記述できるように設計されました
    • 「副作用」とはHTTPリクエスト、引数の変更、ランダム値生成など関数外部で状態/動作を変更するコードです
  • ミドルウェアは標準Reduxデータフローに追加ステップを導入します
    • ミドルウェアはdispatchに渡される他の値をインターセプトできます
    • ミドルウェアはdispatchgetStateにアクセス可能なため、非同期ロジックの一部として追加アクションをディスパッチできます
  • Reduxの「Thunk」ミドルウェアは関数をdispatchに渡せるようにします
    • 「Thunk」関数により使用するReduxストアを知らなくても非同期ロジックを事前に記述できます
    • Redux thunk関数はdispatchgetStateを引数として受け取り、「このデータはAPIレスポンスから受信した」といったアクションをディスパッチできます

次のステップ

Reduxの使用方法に関する核心的な部分をすべて網羅しました!以下の方法を確認しました:

  • ディスパッチされたアクションに基づいて状態を更新するリデューサーの記述

  • リデューサー、エンハンサー、ミドルウェアを使用したReduxストアの作成と設定

  • アクションをディスパッチする非同期ロジックを記述するためのミドルウェアの使用

パート7:標準的なReduxパターンでは、実際のReduxアプリケーションで一般的に使用されるコードパターンをいくつか見ていきます。これによりコードの一貫性が向上し、アプリケーションの成長に合わせてより良くスケールできるようになります。