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

Reduxの基礎、パート6:非同期ロジックとデータフェッチング

Reduxの基礎、パート6:非同期ロジックとデータフェッチング

学習内容
  • Reduxのデータフローが非同期データでどのように機能するか
  • Reduxミドルウェアを非同期ロジックに使用する方法
  • 非同期リクエスト状態を処理するためのパターン
前提条件
  • サーバーからデータをフェッチおよび更新するためにAJAXリクエストを使用することに慣れていること
  • Promisesを含むJSでの非同期ロジックの理解

はじめに

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

これまで、私たちが扱ってきたすべてのデータは、React+Reduxクライアントアプリケーションの内部に直接ありました。ただし、ほとんどの実際のアプリケーションは、HTTP API呼び出しを行ってアイテムをフェッチおよび保存することにより、サーバーからのデータを操作する必要があります。

このセクションでは、APIからtodosをフェッチし、APIに保存して新しいtodosを追加するようにtodoアプリを更新します。

注意

このチュートリアルでは、Reduxの背後にある原則と概念を説明するために、今日Reduxでアプリを構築するための正しいアプローチとして教えられているRedux Toolkitを使用した「現代的なRedux」パターンよりも多くのコードを必要とする、古いスタイルのReduxロジックパターンを意図的に示していることに注意してください。これは、本番環境に対応したプロジェクトを意味するものではありません。

Redux Toolkitを使用して「現代的なRedux」を使用する方法については、これらのページをご覧ください。

ヒント

Redux Toolkitには、RTK QueryデータフェッチングおよびキャッシュAPIが含まれています。RTK Queryは、Reduxアプリ向けに構築されたデータフェッチングおよびキャッシュソリューションであり、データフェッチングを管理するために任意のサンクまたはリデューサーを記述する必要性を排除できます。特に、データフェッチングのデフォルトのアプローチとしてRTK Queryを教えており、RTK Queryはこのページに示されているものと同じパターンに基づいて構築されています。

Reduxエッセンシャルズ、パート7:RTKクエリの基礎で、データフェッチングに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で定義されています。

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

Reduxミドルウェアと副作用

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

以前、Reduxリデューサーには「副作用」を含めてはならないと述べました。「副作用」とは、関数の値を返す以外に、外部から認識できる状態または動作への変更のことです。一般的な副作用には、次のようなものがあります。

  • コンソールに値をロギングする
  • ファイルを保存する
  • 非同期タイマーを設定する
  • AJAX HTTPリクエストを行う
  • 関数の外部に存在する何らかの状態を変更したり、関数への引数を変更したりする
  • 乱数や一意のランダムID(Math.random()Date.now()など)を生成する

ただし、実際のアプリでは、これらの種類のことをどこかで行う必要があります。したがって、リデューサーに副作用を入れることができない場合、どこに入れることができるでしょうか?

Reduxミドルウェアは、副作用を持つロジックの記述を可能にするように設計されました。.

パート4で述べたように、Reduxミドルウェアは、ディスパッチされたアクションが表示されたときに、何かをロギングしたり、アクションを変更したり、アクションを遅らせたり、非同期呼び出しを行ったり、その他何でも実行できます。また、ミドルウェアは実際のstore.dispatch関数の周りにパイプラインを形成するため、これは、ミドルウェアがその値をインターセプトしてリデューサーに到達させない限り、実際にはプレーンなアクションオブジェクトではないものをdispatchに渡すことができることも意味します。

ミドルウェアは、dispatchgetStateにもアクセスできます。つまり、ミドルウェアに非同期ロジックを記述しても、アクションをディスパッチすることでReduxストアと対話する機能は引き続き使用できるということです。

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

Reduxストアと対話するような非同期ロジックを記述できるようにするミドルウェアのいくつかの例を見てみましょう。

1つの可能性は、特定のアクションタイプを検索し、それらのアクションが表示されたときに非同期ロジックを実行するミドルウェアを記述することです。次に例を示します。

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の回答を参照してください。

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

最後のセクションのミドルウェアはどちらも非常に具体的で、1つのことしか行いませんでした。ミドルウェア自体とは別に、任意の非同期ロジックを事前に記述し、それでもストアと対話できるようにdispatchgetStateにアクセスする方法があれば便利です。

「アクション」オブジェクトの代わりに関数dispatchに渡すことができるミドルウェアを作成したらどうでしょうか?ミドルウェアで「アクション」が実際には関数であるかどうかを確認し、関数である場合は、すぐにその関数を呼び出すことができます。これにより、ミドルウェアの定義外で、個別の関数で非同期ロジックを記述できます。

そのミドルウェアは次のようになります。

非同期関数ミドルウェアの例
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アプリの全体的なデータフローにどのように影響するのでしょうか?

通常のactionと同様に、まずアプリケーションでユーザーイベント(ボタンのクリックなど)を処理する必要があります。次に、dispatch()を呼び出し、プレーンなactionオブジェクト、関数、またはmiddlewareが探せる他の何らかの値を渡します。

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

以前、Reduxの通常の同期的なデータフローを表す図を見ました。Reduxアプリに非同期ロジックを追加すると、middlewareがAJAXリクエストなどのロジックを実行し、その後actionをディスパッチできる追加のステップが追加されます。これにより、非同期データフローはこのようになります。

Redux async data flow diagram

Redux Thunk Middlewareの使用

Reduxには、公式バージョンの「非同期関数middleware」であるRedux "Thunk" middlewareがすでにあります。Thunk middlewareを使用すると、引数としてdispatchgetStateを受け取る関数を作成できます。Thunk関数内には必要な非同期ロジックを含めることができ、そのロジックは必要に応じてactionをディスパッチしたり、ストアの状態を読み取ることができます。

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

情報

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

およびこれらの投稿

ストアの構成

Redux thunk middlewareは、redux-thunkというパッケージとしてNPMで入手できます。アプリで使用するには、このパッケージをインストールする必要があります。

npm install redux-thunk

インストールが完了したら、todoアプリのReduxストアを更新して、そのmiddlewareを使用するようにできます。

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

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

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

サーバーからのTodosのフェッチ

現在、todoエントリはクライアントのブラウザにのみ存在できます。アプリが起動したときにサーバーからtodoリストをロードする方法が必要です。

まず、/fakeApi/todosエンドポイントにAJAX呼び出しを行ってtodoオブジェクトの配列をリクエストし、その配列をpayloadとして含むactionをディスパッチするthunk関数を記述します。これはtodo機能全般に関連するため、todosSlice.jsファイルにthunk関数を記述します。

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 ReactDOM from 'react-dom'
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)

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

ページをリロードしても、UIに目に見える変化はありません。ただし、Redux DevTools拡張機能を開くと、'todos/todosLoaded' actionがディスパッチされたことが確認でき、フェイクサーバーAPIによって生成されたいくつかのtodoオブジェクトが含まれているはずです。

Devtools - todosLoaded action contents

actionをディスパッチしても、状態を変更するようなことは何も起こらないことに注意してください。状態を更新するには、todos reducerでこのactionを処理する必要があります。

reducerにケースを追加して、このデータをストアにロードしましょう。サーバーからデータをフェッチしているので、既存のtodoを完全に置き換えたいので、action.payload配列を返して、新しいtodosの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 })
}

actionをディスパッチするとすぐにストアが更新されるため、ディスパッチ後にthunk内でgetStateを呼び出して、更新された状態の値を読み取ることもできます。たとえば、'todos/todosLoaded' actionをディスパッチする前と後に、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' actionをディスパッチする代わりに、初期データでサーバーにAPI呼び出しを行い、サーバーが新しく保存されたtodoアイテムのコピーを返送するのを待って、その後、そのtodoアイテムでactionをディスパッチする必要があります。

ただし、このロジックをthunk関数として書き始めようとすると、問題が発生します。thunkを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をパラメータとして受け取る1つの関数を作成する必要がありますが、その後、API呼び出しを行うためにtext値を使用できるように、実際のthunk関数を作成する必要があります。外側の関数は、コンポーネントのdispatchに渡せるように、thunk関数を返す必要があります。

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に渡されるものを準備するための関数を記述するこのパターンは、「action creator」パターンと呼ばれ、これについては次のセクションで詳しく説明します。

更新された'todos/todoAdded' actionがディスパッチされていることが確認できます。

Devtools - async todoAdded action contents

ここで最後に変更する必要があるのは、todos reducerの更新です。/fakeApi/todosにPOSTリクエストを行うと、サーバーは完全に新しいtodoオブジェクト(新しいID値を含む)を返します。つまり、reducerは新しい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 - async todoAdded state diff

ヒント

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

学んだこと

これで、フェイクサーバーAPIへのAJAX呼び出しを行う「thunk」関数を使用して、todoアイテムのリストをフェッチし、新しいtodoアイテムを保存できるように、todoアプリを正常に更新しました。

この過程で、Redux middlewareを使用して、非同期呼び出しを行い、非同期呼び出しが完了した後にactionをディスパッチすることで、ストアと対話する方法を確認しました。

現在のアプリの様子は次のとおりです。

まとめ
  • Reduxミドルウェアは、副作用を持つロジックの記述を可能にするように設計されました。
    • 「副作用」とは、AJAX呼び出し、関数引数の変更、または乱数値の生成など、関数外の状態/動作を変更するコードです。
  • Middlewareは、標準のReduxデータフローに余分なステップを追加します。
    • Middlewareは、dispatchに渡される他の値をインターセプトできます。
    • MiddlewareはdispatchgetStateにアクセスできるため、非同期ロジックの一部としてさらにactionをディスパッチできます。
  • Redux "Thunk" middlewareを使用すると、関数をdispatchに渡すことができます。
    • 「Thunk」関数を使用すると、どのReduxストアが使用されているかを知らなくても、事前に非同期ロジックを記述できます。
    • Redux thunk関数は、引数としてdispatchgetStateを受け取り、「このデータはAPIレスポンスから受信されました」のようなactionをディスパッチできます。

次は何を学ぶか?

これで、Reduxの使用方法に関するすべての重要な要素を説明しました。これまでに、次の方法を学びました。

  • ディスパッチされたactionに基づいて状態を更新するreducerを記述する。
  • reducer、enhancer、およびmiddlewareを使用して、Reduxストアを作成および構成する。
  • middlewareを使用して、actionをディスパッチする非同期ロジックを記述する。

パート7:標準Reduxパターンでは、コードの一貫性を高め、アプリケーションの成長に合わせてより適切にスケーリングするために、実際のReduxアプリで一般的に使用されるいくつかのコードパターンについて説明します。