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エッセンシャルズ」チュートリアル全体。これは、実際のアプリ向けにRedux Toolkitを使用して「Reduxを正しく使用する方法」を教えます。すべてのRedux学習者は、「エッセンシャルズ」チュートリアルを読むことをお勧めします!
- Reduxファンダメンタルズ、パート8:Redux Toolkitによる現代的なRedux。これは、以前のセクションの低レベルの例を現代的なRedux Toolkit相当に変換する方法を示しています。
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
に渡すことができることも意味します。
ミドルウェアは、dispatch
とgetState
にもアクセスできます。つまり、ミドルウェアに非同期ロジックを記述しても、アクションをディスパッチすることで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つのことしか行いませんでした。ミドルウェア自体とは別に、任意の非同期ロジックを事前に記述し、それでもストアと対話できるようにdispatch
とgetState
にアクセスする方法があれば便利です。
「アクション」オブジェクトの代わりに関数を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 Thunk Middlewareの使用
Reduxには、公式バージョンの「非同期関数middleware」であるRedux "Thunk" middlewareがすでにあります。Thunk middlewareを使用すると、引数としてdispatch
とgetState
を受け取る関数を作成できます。Thunk関数内には必要な非同期ロジックを含めることができ、そのロジックは必要に応じてactionをディスパッチしたり、ストアの状態を読み取ることができます。
非同期ロジックをthunk関数として記述することで、事前にどのReduxストアを使用しているかを知らなくても、そのロジックを再利用できます。.
「thunk」という単語はプログラミング用語で、「遅延した処理を行うコードの一部」を意味します。thunkの使用方法の詳細については、thunkの使用ガイドページを参照してください。
およびこれらの投稿
ストアの構成
Redux thunk middlewareは、redux-thunk
というパッケージとしてNPMで入手できます。アプリで使用するには、このパッケージをインストールする必要があります。
npm install redux-thunk
インストールが完了したら、todoアプリのReduxストアを更新して、そのmiddlewareを使用するようにできます。
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関数を記述します。
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
に配置してみましょう。
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オブジェクトが含まれているはずです。
actionをディスパッチしても、状態を変更するようなことは何も起こらないことに注意してください。状態を更新するには、todos reducerでこのactionを処理する必要があります。
reducerにケースを追加して、このデータをストアにロードしましょう。サーバーからデータをフェッチしているので、既存のtodoを完全に置き換えたいので、action.payload
配列を返して、新しいtodosのstate
値にすることができます。
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テキストが何であるべきかを知りません。
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関数を返す必要があります。
// 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>
コンポーネントで使用できます。
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
に渡すことができます。
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がディスパッチされていることが確認できます。
ここで最後に変更する必要があるのは、todos reducerの更新です。/fakeApi/todos
にPOSTリクエストを行うと、サーバーは完全に新しいtodoオブジェクト(新しいID値を含む)を返します。つまり、reducerは新しいIDを計算したり、他のフィールドを埋めたりする必要はありません。新しいtodoアイテムを含む新しいstate
配列を作成するだけで済みます。
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の追加が正しく機能します。
Thunk関数は、非同期ロジックと同期ロジックの両方に使用できます。Thunkは、dispatch
とgetState
へのアクセスが必要な再利用可能なロジックを記述する方法を提供します。
学んだこと
これで、フェイクサーバーAPIへのAJAX呼び出しを行う「thunk」関数を使用して、todoアイテムのリストをフェッチし、新しいtodoアイテムを保存できるように、todoアプリを正常に更新しました。
この過程で、Redux middlewareを使用して、非同期呼び出しを行い、非同期呼び出しが完了した後にactionをディスパッチすることで、ストアと対話する方法を確認しました。
現在のアプリの様子は次のとおりです。
- Reduxミドルウェアは、副作用を持つロジックの記述を可能にするように設計されました。
- 「副作用」とは、AJAX呼び出し、関数引数の変更、または乱数値の生成など、関数外の状態/動作を変更するコードです。
- Middlewareは、標準のReduxデータフローに余分なステップを追加します。
- Middlewareは、
dispatch
に渡される他の値をインターセプトできます。 - Middlewareは
dispatch
とgetState
にアクセスできるため、非同期ロジックの一部としてさらにactionをディスパッチできます。
- Middlewareは、
- Redux "Thunk" middlewareを使用すると、関数を
dispatch
に渡すことができます。- 「Thunk」関数を使用すると、どのReduxストアが使用されているかを知らなくても、事前に非同期ロジックを記述できます。
- Redux thunk関数は、引数として
dispatch
とgetState
を受け取り、「このデータはAPIレスポンスから受信されました」のようなactionをディスパッチできます。
次は何を学ぶか?
これで、Reduxの使用方法に関するすべての重要な要素を説明しました。これまでに、次の方法を学びました。
- ディスパッチされたactionに基づいて状態を更新するreducerを記述する。
- reducer、enhancer、およびmiddlewareを使用して、Reduxストアを作成および構成する。
- middlewareを使用して、actionをディスパッチする非同期ロジックを記述する。
パート7:標準Reduxパターンでは、コードの一貫性を高め、アプリケーションの成長に合わせてより適切にスケーリングするために、実際のReduxアプリで一般的に使用されるいくつかのコードパターンについて説明します。