使用方法ガイド
コアのReduxライブラリは意図的に無意見です。これにより、ストアスのセットアップ方法、stateに含める内容、リデューサーの構築方法など、すべてをどのように処理するかを決定できます。
柔軟性があるため、場合によってはこれが良いのですが、常にその柔軟性は必要ではありません。場合によっては、可能な限りシンプルな方法で、すぐに優れた既定の動作を手に入れたいと思うかもしれません。あるいは、大規模なアプリケーションを作成していて、同じようなコードを記述していることに気づき、手作業で記述するコードの量を削減したいと思うかもしれません。
クイックスタートページで説明されているように、Redux Toolkitの目標は、一般的なReduxのユースケースを簡素化することです。Reduxでやりたいことすべてに対する完全なソリューションであることを目的としているわけではありませんが、記述する必要があるRedux関連のコードのかなりの部分をさらにシンプルにするはずです(場合によっては手で記述するコードの一部を完全に排除できることもあります)。
Redux Toolkitは、アプリケーションで使用できるいくつかの個別の関数をエクスポートし、Reduxと一般的に使用される他のいくつかのパッケージ(ReselectやRedux-Thunkなど)への依存関係を追加します。これにより、新しいプロジェクトであろうと大規模な既存のアプリケーションの更新であろうと、それらをアプリケーションでどのように使用するかを決定できます。
Redux ToolkitがRedux関連のコードを向上させるのにどのように役立つかを見てみましょう。
ストアのセットアップ
すべてのReduxアプリケーションは、Reduxストアの設定と作成を行う必要があります。これには通常、複数のステップが含まれます
- ルートリデューサー関数のインポートまたは作成
- ミドルウェアのセットアップ、少なくとも非同期ロジックを処理するためのミドルウェアが1つ含まれます
- Redux DevTools Extensionの設定
- アプリケーションが開発用か本番用に構築されているかどうかに基づいて、一部のロジックを変更すること
手動ストアセットアップ
Redux ドキュメントの ストアの設定 ページの次の例は、ストアの一般的なセットアップ処理を示しています
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
この例は読みやすいですが、このプロセスは常に単純ではありません
- Redux の基本的な
createStore
関数は、位置引数をとります:(rootReducer, preloadedState, enhancer)
。引数がどれに該当するかを覚えておくのが難しい場合があります。 - 特に複数の構成を追加しようとしている場合は、ミドルウェアとエンハンサーの設定プロセスが混乱する可能性があります。
- Redux DevTools 拡張機能のドキュメントは当初、拡張機能が使用可能かどうかを確認するためにグローバル名前空間を確認する手書きのコードを使用することを示唆しています。多くのユーザーはこれらのスニペットをコピーして貼り付けますが、これによりセットアップコードが読み取りにくくなります。
configureStore
によるストアセットアップの簡略化
configureStore
は以下の方法でこれらの問題を解決します
- 読みやすい「名前付き」パラメータが記載されたオプションオブジェクトを使用できます
- ストアに追加するミドルウェアとエンハンサーの配列を提供でき、
applyMiddleware
とcompose
を自動的に呼び出します - Redux DevTools 拡張機能が自動的に有効化されます
さらに、configureStore
は既定でいくつかのミドルウェアを追加し、それぞれに特定の目標があります
redux-thunk
はコンポーネントの外側で同期ロジックと非同期ロジックの両方で機能するために最も一般的に使用されるミドルウェアです- 開発において、状態の変異やシリアル化できない値の使用など、一般的な間違いをチェックするミドルウェアです。
つまり、ストアの設定コード自体が少し短く、読みやすくなります。また、すぐに適切な既定の動作が得られます。
使用する最も簡単な方法は、単にルートリデューサー関数を reducer
という名前のパラメータとして渡すことです
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer,
})
export default store
「スライスリデューサー」
が記載されたオブジェクトを渡すこともできます。すると、configureStore
はcombineReducers
を呼び出します
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
})
export default store
これはリデューサーの 1 つのレベルにのみ適用されることに注意してください。リデューサーをネストする場合は、ネストを処理するために combineReducers
を自分で呼び出す必要があります。
ストアのセットアップをカスタマイズする必要がある場合は、追加のオプションを渡すことができます。ホットリローディングの例が Redux Toolkit を使用した場合にどのように表示されるかを示します
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
preloadedState,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(monitorReducersEnhancer),
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
middleware
引数を指定すると、configureStore
は一覧表示されたミドルウェアのみを使用します。カスタムミドルウェアと 既定のもの両方を一緒に使用したい場合は、コールバック表記を使用して、getDefaultMiddleware
を呼び出して、返される middleware
配列に結果を含めることができます。
リデューサーの記述
リデューサー は、最も重要な Redux の概念です。典型的なリデューサー関数は、次のようにする必要があります。
- アクションオブジェクトの
type
フィールドを見て、どのように反応すべきかを確認します - 変更が必要な状態の部分のコピーを作成し、それらのコピーのみを変更することで変更不可能に状態を更新します
リデューサでは任意の条件付きロジックを使用できますが、1つのフィールドに対してあり得る複数の値を扱うには、単刀直入な方法であるswitch
文が最も一般的です。しかし、多くの人はswitch文を好みません。Reduxの資料では、アクションタイプに基づいてルックアップテーブルとして機能する関数の記述の例が示されていますが、この関数をユーザーが自分でカスタマイズする必要があります。
リデューサの記述に関するもう1つの一般的な問題点は、状態を不変に更新することです。JavaScriptは可変言語であり、ネストされた不変データを手で更新することは難しく、間違いが発生しやすいです。
createReducer
によるリデューサの簡略化
「ルックアップテーブル」アプローチは普及しているため、Redux Toolkitには、Reduxの資料で示されているものと同様のcreateReducer
関数が含まれています。ただし、当社のcreateReducer
ユーティリティには、さらに便利にする特別な「マジック」があります。内部的にImmerライブラリを使用することで、データを「変異」するコードを記述できますが、実際には更新は不変に適用されます。これにより、リデューサで意図せずに状態を変異させることが事実上不可能になります。
一般的に、switch
文を使用するReduxリデューサは、すべてcreateReducer
を直接使用するように変換できます。switchの各case
は、createReducer
に渡されるオブジェクトのキーになります。オブジェクトのスプレッドや配列のコピーなどの不変の更新ロジックは、おそらく直接の「変異」に変換できます。そのまま不変の更新を保持して更新されたコピーを返すことも問題ありません。
以下はcreateReducer
の使用方法の例です。switch文と不変の更新を使用した一般的な「Todoリスト」リデューサから始めます。
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return state.concat(action.payload)
}
case 'TOGGLE_TODO': {
const { index } = action.payload
return state.map((todo, i) => {
if (i !== index) return todo
return {
...todo,
completed: !todo.completed,
}
})
}
case 'REMOVE_TODO': {
return state.filter((todo, i) => i !== action.payload.index)
}
default:
return state
}
}
state.concat()
を明示的に呼び出して新しいTodoエントリを含むコピーされた配列を返し、state.map()
を呼び出して切り替えケースのコピーされた配列を返し、オブジェクトスプレッド演算子を使用して更新する必要があるTodoのコピーを作成することに注意してください。
createReducer
を使用すると、この例を大幅に短縮できます。
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// "mutate" the object by overwriting a field
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter((todo, i) => i !== action.payload.index)
})
})
状態を「変異」する機能は、階層構造が深い状態を更新する場合に特に役立ちます。次のような複雑で負担のかかるコード
case "UPDATE_VALUE":
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
は次のようになります。
updateValue(state, action) {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
}
はるかに優れています。
createReducer
を使用する際の考慮事項
Redux ToolkitのcreateReducer
関数は非常に役立つ可能性がありますが、次のことに注意してください。
- 「変異」コードは
createReducer
関数の内部でのみ正しく機能します。 - Immerでは、「変異」するドラフト状態と新しい状態値を返すことを混在させることはできません。
詳細については、createReducer
APIリファレンスを参照してください。
アクションの作成
Reduxでは、アクションオブジェクトを作成するプロセスをカプセル化する「アクション作成」関数を記述することが推奨されています。これは厳密には必須ではありませんが、Redux使用の標準部分です。
ほとんどのアクションクリエーターは非常にシンプルです。パラメーターを受け取り、特定の type
フィールドとアクション内のパラメーターを持つアクションオブジェクトを返します。これらのパラメーターは通常 payload
と呼ばれるフィールドに配置されます。これはアクションオブジェクトのコンテンツを編成するための Flux Standard Action 規約の一部です。一般的なアクションクリエーターは次のようになります。
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}
createAction
を使用したアクションクリエーターの定義
アクションクリエーターを手書きするのは面倒な場合があります。Redux Toolkit には createAction
という関数が用意されており、指定されたアクションタイプを使用してアクションクリエーターを生成し、その引数を payload
フィールドに変換します。
const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
createAction
は「準備コールバック」引数も受け取ります。これにより、結果の payload
フィールドをカスタマイズし、オプションで meta
フィールドを追加できます。準備コールバックでアクションクリエーターを定義する方法の詳細については、createAction
API リファレンス を参照してください。
アクションクリエーターをアクションタイプとして使用
Redux リデューサーは、状態をどのように更新するかを決定するために特定のアクションタイプを探す必要があります。通常、これはアクションタイプ文字列とアクションクリエーター関数を別々に定義することで行われます。Redux Toolkit の createAction
関数では、アクションクリエーターに type
フィールドとしてアクションタイプを定義することで、この作業が簡素化されます。
const actionCreator = createAction('SOME_ACTION_TYPE')
console.log(actionCreator.type)
// "SOME_ACTION_TYPE"
const reducer = createReducer({}, (builder) => {
// if you use TypeScript, the action type will be correctly inferred
builder.addCase(actionCreator, (state, action) => {})
// Or, you can reference the .type field:
// if using TypeScript, the action type cannot be inferred that way
builder.addCase(actionCreator.type, (state, action) => {})
})
つまり、別のアクションタイプ変数を記述または使用したり、const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"
のようなアクションタイプの名前と値を繰り返す必要はありません。
これらのアクションクリエーターを switch ステートメントで使用したい場合は、actionCreator.type
を自分で参照する必要があります。
const actionCreator = createAction('SOME_ACTION_TYPE')
const reducer = (state = {}, action) => {
switch (action.type) {
// ERROR: this won't work correctly!
case actionCreator: {
break
}
// CORRECT: this will work as expected
case actionCreator.type: {
break
}
}
}
状態のスライスの作成
Redux 状態は通常、「スライス」に編成され、combineReducers
に渡されるリデューサーによって定義されます。
import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
})
この例では、users
と posts
の両方が「スライス」と見なされます。両方のリデューサー
- 初期値を含む、状態の一部を「所有」する
- その状態がどのように更新されるか定義する
- 状態の更新をもたらす特定のアクションを定義する
一般的なアプローチは、スライスのリデューサー関数を独自のファイルに定義し、アクションクリエーターを 2 番目のファイルに定義することです。どちらの関数も同じアクションタイプを参照する必要があるため、通常それらは 3 番目のファイルで定義され、両方の場所にインポートされます。
// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// omit implementation
}
default:
return state
}
}
ここで本当に必要なのはリデューサー自体だけです。他の部分を検討する。
- アクションタイプを両方でインライン文字列として記述できた可能性があります
- アクションクリエーターは優れていますが、Redux を使用するのに必須ではありません。コンポーネントは
connect
にmapDispatch
引数を渡さずにスキップし、this.props.dispatch({type : "CREATE_POST", payload : {id : 123, title : "Hello World"}})
を呼び出すことができます。 - 複数ファイルを作成する唯一の理由は、何をすべきかによってコードを分離することが一般的であるためです。
"ダック" ファイル構造 は、特定のスライスの Redux 関連ロジックをすべて単一のファイルに配置することを提案しています。次のようなものがあります。
// postsDuck.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// Omit actual code
break
}
default:
return state
}
}
複数のファイルを必要とせず、アクションタイプ定数の冗長なインポートを削除できるため、これにより状況は簡素化されます。ただし、アクションタイプとアクションクリエーターを手動で記述する必要があります。
オブジェクト内の関数定義
最新の JavaScript では、オブジェクト内にキーと関数の両方を定義する合法的ないくつかの方法があります(これは Redux に固有ではありません)。また、さまざまなキー定義と関数定義を組み合わせて使用できます。たとえば、これらはすべてオブジェクト内に関数定義する合法的な方法です。
const keyName = "ADD_TODO4";
const reducerObject = {
// Explicit quotes for the key name, arrow function for the reducer
"ADD_TODO1" : (state, action) => { }
// Bare key with no quotes, function keyword
ADD_TODO2 : function(state, action){ }
// Object literal function shorthand
ADD_TODO3(state, action) { }
// Computed property
[keyName] : (state, action) => { }
}
"オブジェクトリテラル関数省略形"を使用するのがおそらく最もコードが短くなりますが、好きなアプローチを使用してください。
createSlice
によるスライスの簡略化
このプロセスを簡素化するために、Redux Toolkitには、提供するリデューサ関数の名前をもとに、アクションタイプとアクションクリエイターを自動生成するcreateSlice
関数が含まれています。
createSlice
を使用して投稿の例を以下のように示します
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
console.log(postsSlice)
/*
{
name: 'posts',
actions : {
createPost,
updatePost,
deletePost,
},
reducer
}
*/
const { createPost } = postsSlice.actions
console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
createSlice
はreducers
フィールドで定義されたすべての関数を調べ、提供されるすべての「ケースリデューサ」関数について、リデューサの名前をアクションタイプ自体として使用するアクションクリエイターを生成します。つまり、createPost
リデューサは"posts/createPost"
のアクションタイプになり、createPost()
アクションクリエイターはそのタイプのアクションを返します。
スライスのエクスポートと使用
ほとんどの場合、スライスを定義し、そのアクションクリエイターとリデューサをエクスポートする必要があります。これを行う推奨方法は、ES6の非構造化とエクスポート構文を使用することです。
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
// Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer
必要に応じて、スライスオブジェクト自体を直接エクスポートすることもできます。
このように定義されたスライスは、"Redux Ducks"パターンを使用してアクションクリエイターとリデューサを定義してエクスポートするという概念に非常に似ています。ただし、スライスをインポートしてエクスポートするときに注意すべき、潜在的な問題がいくつかあります。
まず、Reduxアクションタイプは単一のスライスに限定されるものではありません。概念的には、各スライスリデューサはRedux状態の独自のピースを「所有」しますが、すべてのアクションタイプをリッスンして適切に状態を更新できる必要があります。たとえば、多くの異なるスライスは、「ユーザーがログアウト」アクションに対応して、データをクリアするか、初期状態値にリセットしようとする可能性があります。状態の形状を設計してスライスを作成するときに、これを考慮してください。
第二に、2つのモジュールが互いにインポートしようとすると、JSモジュールに「循環参照」の問題が発生する可能性があります。その結果、インポートが未定義になる可能性があり、インポートを必要とするコードが壊れる可能性があります。具体的には、「ダック」またはスライスの場合、別の2つのファイルで定義されたスライスが、もう一方のファイルで定義されたアクションに対応する必要がある場合に発生する可能性があります。
このCodeSandboxの例は、問題を示しています。
この問題が発生した場合は、循環参照を避ける方法でコードを再構築する必要があります。通常、両方のモジュールがインポートして使用できる、別の共通ファイルに共有コードを抽出する必要があります。この場合、createAction
を使用して別のファイルにいくつかの共通のアクションタイプを定義し、それらのアクションクリエイターを各スライスファイルにインポートし、extraReducers
引数を使用して処理できます。
JSにおける循環依存の問題を修正する方法という記事には、この問題に対処する追加の情報と例があります。
非同期ロジックおよびデータのフェッチ
非同期ロジックを有効にするためのミドルウェアの使用
Redux ストアの単独では、非同期ロジックについて何も知りません。同期的にアクションをディスパッチし、ルート・リデューサ関数で呼び出して状態をアップデートし、何かが変わったことを UI に通知する方法のみを知っています。非同期性に関わる操作は、ストアの外で発生している必要があります。
しかし、非同期ロジックでストアとやりとりし、ディスパッチやストアの現在の状態の確認ができる場合が考えられます。そこで Redux ミドルウェア が登場します。これによりストアを拡張し、以下のことが可能になります。
- アクションがディスパッチされたときの追加ロジックの実行(アクションと状態のログ記録など)
- ディスパッチされたアクションの一時停止、修正、遅延、置換、停止
dispatch
とgetState
にアクセスできる追加コードの作成- アクションを傍受し、アクション・オブジェクトの代わりとして実際のディスパッチアクションをディスパッチすることで、
dispatch
がプレーンアクション以外にも、関数やプロミスなどの値を受け入れるようにする方法
ミドルウェアを使用する最も一般的な理由は、さまざまな種類の非同期ロジックがストアとやりとりできるようにすることです。これにより、アクションのディスパッチとストア状態の確認を行うコードを記述できますが、そのロジックは UI から切り離しておくことができます。
Redux 用の非同期ミドルウェアにはさまざまな種類があり、それぞれ異なる構文を使用してロジックを作成できます。最も一般的な非同期ミドルウェアは次のとおりです。
redux-thunk
は、非同期ロジックを直接含むプレーン関数を作成できます。redux-saga
は、ジェネレータ関数を使用して動作の説明を返し、ミドルウェアによって実行されます。redux-observable
は、RxJS のオブザーバブルライブラリを使用して、アクションを処理する関数チェインを作成します。
各ライブラリにはさまざまなユースケースとトレードオフがあります。.
Redux Toolkit の RTK Query データフェッチング API は Redux アプリ向けの専用データフェッチングやキャッシュソリューションであり、データフェッチングを管理するためのあらゆるサンクやリデューサを記述する必要性を排除できます。ぜひお試しいただき、それが自身のアプリのデータフェッチングコードを簡潔にするのに役立つかどうかを確認してください。
独自にデータフェッチングロジックを作成する必要がある場合は、Redux Thunk ミドルウェアを標準アプローチとして使用することをお勧めします。ほとんどの一般的なユースケース(基本的な AJAX データフェッチングなど)に対応できます。さらに、サンクで async/await
構文を使用すると、それらが読みやすくなります。
Redux Toolkit の configureStore
関数は、デフォルトでサンクミドルウェアを自動的にセットアップします。そのため、アプリケーションコードの一部としてサンクを作成できます。
スライスで非同期ロジックを定義する
Redux Toolkit は、サンク関数の作成用の特別な API や構文は現在提供していません。特に、これらは createSlice()
呼び出しの一部として定義することはできません。通常の Redux コードと同じように、リデューサロジックとは別々に作成する必要があります。
一般的に、Thunk は、`dispatch(dataLoaded(response.data))` などの単純なアクションをディスパッチします。
多くの Redux アプリは、「タイプごとのフォルダー」という方法を使用してコードを構成しました。この構造では、Thunk アクション クリエーターは通常、単純なアクション クリエーターと一緒に「actions」ファイルに定義されます。
別個の「actions」ファイルがないため、これらの Thunk を「slice」ファイルに直接書くことが理にかなっています。そうすることで、スライスからの単純なアクション クリエーターにアクセスでき、Thunk 関数が存在する場所を見つけやすくなります。
Thunk が含まれる一般的なスライス ファイルは次のようになります
// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: [],
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
},
},
})
// Destructure and export the plain action creators
export const { usersLoading, usersReceived } = usersSlice.actions
// Define a thunk that dispatches those action creators
const fetchUsers = () => async (dispatch) => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}
Redux のデータフェッチ パターン
Redux のデータ フェッチ ロジックは、一般的に予測可能なパターンに従います。
- リクエストが進行中であることを示すために、リクエストの前に「start」アクションがディスパッチされます。これを使用して、読み込み状態を追跡し、重複したリクエストをスキップしたり、UI に読み込みインジケーターを表示したりできます。
- 非同期リクエストが行われます
- リクエストの結果によっては、非同期ロジックは結果データを含む「success」アクションか、エラー詳細を含む「failure」アクションのいずれかをディスパッチします。リデューサー ロジックはどちらの場合でも読み込み状態をクリアし、success ケースから結果データを処理するか、潜在的な表示のためにエラー値を格納します。
これらのステップは必須ではありませんが、Redux チュートリアルで推奨されるパターンとして提案されています。
一般的な実装は次のようになります
const getRepoDetailsStarted = () => ({
type: 'repoDetails/fetchStarted',
})
const getRepoDetailsSuccess = (repoDetails) => ({
type: 'repoDetails/fetchSucceeded',
payload: repoDetails,
})
const getRepoDetailsFailed = (error) => ({
type: 'repoDetails/fetchFailed',
error,
})
const fetchIssuesCount = (org, repo) => async (dispatch) => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}
ただし、このアプローチを使用してコードを作成するのは面倒です。各種類のリクエストには、同様の実装を繰り返し行う必要があります。
- 3 つの異なるケースに対して、一意のアクション タイプを定義する必要があります。
- 通常、これらのアクション タイプごとに、対応するアクション クリエーター関数が存在します。
- 正しいシーケンスで正しいアクションをディスパッチする Thunk を記述する必要があります。
createAsyncThunk
は、アクション タイプとアクション クリエーターを生成し、それらのアクションをディスパッチする Thunk を生成することでこのパターンを抽象化します。
createAsyncThunk
を使用した非同期リクエスト
開発者としては、おそらく API リクエストを行うのに必要な実際のロジック、Redux アクション履歴ログに表示されるアクション タイプ名、およびリデューサーがフェッチしたデータを処理する方法が最も懸念されます。複数の aksi\yon タイプを定義し、それらのアクションを正しい順序でディスパッチするという繰り返し的な詳細が問題ではありません。
createAsyncThunk
はこのプロセスを簡略化します。アクション タイプのプレフィックス用の文字列と、実際の非同期ロジックを実行して結果を含むプロミスを返すペイロード クリエーター コールバックを提供するだけです。その見返りとして、createAsyncThunk
は、あなたが返したプロミスと、リデューサーで処理できるアクション タイプに基づいて適切なアクションをディスパッチする Thunk を提供します。
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
Thunk アクション クリエーターは単一の引数を受け付けます。この引数は、ペイロード クリエーター コールバックへの最初の引数として渡されます。
ペイロード クリエーターは、標準の Redux Thunk 関数に通常渡されるパラメーターを含む thunkAPI
オブジェクトも受け取り、自動生成された一意のランダムなリクエスト ID 文字列と AbortController.signal
オブジェクト を受け取ります。
interface ThunkAPI {
dispatch: Function
getState: Function
extra?: any
requestId: string
signal: AbortSignal
}
必要に応じて、これらを使用してペイロード コールバック内で、最終的な結果が何であるかを決定できます。
正規化されたデータの管理
ほとんどのアプリケーションでは、ネストまたはリレーショナルなデータに対処します。データの正規化の目標は、データをステートで効果的に整理することです。これは、通常、id のキーを持つオブジェクトとしてコレクションを格納し、id のソートされた配列を格納することで行います。さらに詳しい説明と詳細については、Redux ドキュメントの「正規化されたステートシェイプ」ページで素晴らしいリファレンスがあります。
手動での正規化
データの正規化には特別なライブラリは必要ありません。次のようなシェイプのデータを返す fetchAll API リクエストのレスポンスを手動で正規化する方法の簡単な例を次に示します。{` users: [{id: 1, first_name: 'normalized', last_name: 'person'}] }`}、手書きのロジックを使用します
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
return response.data
})
export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
// reduce the collection by the id property into a shape of { 1: { ...user }}
const byId = action.payload.users.reduce((byId, user) => {
byId[user.id] = user
return byId
}, {})
state.entities = byId
state.ids = Object.keys(byId)
})
},
})
このコードを書くことはできますが、特に複数のタイプのデータを処理する場合には、反復的になります。さらに、この例ではエントリをステートにロードする方法のみを処理しており、更新は処理しません。
normalizr を使用した正規化
normalizr はデータを正規化するための既存の人気ライブラリです。Redux なしで単独で使用できますが、Redux と組み合わせて使用されることがよくあります。一般的な使用方法は、API レスポンスからコレクションをフォーマットしてから、リデューサーで処理することです。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { normalize, schema } from 'normalizr'
import userAPI from './userAPI'
const userEntity = new schema.Entity('users')
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// Normalize the data before passing it to our reducer
const normalized = normalize(response.data, [userEntity])
return normalized.entities
})
export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.entities = action.payload.users
state.ids = Object.keys(action.payload.users)
})
},
})
手書きバージョンと同様に、これは追加のエントリをステートに追加したり、後で更新したりすることは処理しません。受信したものをすべてロードするだけです。
createEntityAdapter を使用した正規化
Redux Toolkit の createEntityAdapter API は、コレクションを取得して { ids: [], entities: {} } のシェイプに入れてスライスにデータを格納するための標準化された方法を提供します。この事前定義されたステートシェイプとともに、リデューサー関数とセレクターのセットを生成し、そのデータを操作する方法を認識します。
import {
createSlice,
createAsyncThunk,
createEntityAdapter,
} from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data
})
export const updateUser = createAsyncThunk('users/updateOne', async (arg) => {
const response = await userAPI.updateUser(arg)
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data
})
export const usersAdapter = createEntityAdapter()
// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
// If you want to track 'loading' or other keys, you would initialize them here:
// `getInitialState({ loading: false, activeRequestId: null })`
const initialState = usersAdapter.getInitialState()
export const slice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload
usersAdapter.updateOne(state, { id, changes })
})
},
})
const reducer = slice.reducer
export default reducer
export const { removeUser } = slice.actions
この使用例の完全なコードを CodeSandbox で表示できます
正規化ライブラリに createEntityAdapter を使用する
normalizr または他の正規化ライブラリを既に使用している場合は、createEntityAdapter と組み合わせて使用することを検討できます。上記の例の展開として、payload をフォーマットするために normalizr を使用する方法、それから createEntityAdapter 提供のユーティリティを活用する方法のデモを以下に示します。
デフォルトでは、setAll、addMany、upsertMany CRUD メソッドはエンティティの配列を想定しています。ただし、代替として { 1: { id: 1, ... }} のシェイプのオブジェクトを渡すこともできます。これにより、事前に正規化されたデータの挿入が容易になります。
// features/articles/articlesSlice.js
import {
createSlice,
createEntityAdapter,
createAsyncThunk,
createSelector,
} from '@reduxjs/toolkit'
import fakeAPI from '../../services/fakeAPI'
import { normalize, schema } from 'normalizr'
// Define normalizr entity schemas
export const userEntity = new schema.Entity('users')
export const commentEntity = new schema.Entity('comments', {
commenter: userEntity,
})
export const articleEntity = new schema.Entity('articles', {
author: userEntity,
comments: [commentEntity],
})
const articlesAdapter = createEntityAdapter()
export const fetchArticle = createAsyncThunk(
'articles/fetchArticle',
async (id) => {
const data = await fakeAPI.articles.show(id)
// Normalize the data so reducers can load a predictable payload, like:
// `action.payload = { users: {}, articles: {}, comments: {} }`
const normalized = normalize(data, articleEntity)
return normalized.entities
}
)
export const slice = createSlice({
name: 'articles',
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Handle the fetch result by inserting the articles here
articlesAdapter.upsertMany(state, action.payload.articles)
})
},
})
const reducer = slice.reducer
export default reducer
// features/users/usersSlice.js
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'
const usersAdapter = createEntityAdapter()
export const slice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// And handle the same fetch result by inserting the users here
usersAdapter.upsertMany(state, action.payload.users)
})
},
})
const reducer = slice.reducer
export default reducer
// features/comments/commentsSlice.js
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'
const commentsAdapter = createEntityAdapter()
export const slice = createSlice({
name: 'comments',
initialState: commentsAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Same for the comments
commentsAdapter.upsertMany(state, action.payload.comments)
})
},
})
const reducer = slice.reducer
export default reducer
この normalizr 使用例の完全なコードを CodeSandbox で表示できます
createEntityAdapter でセレクターを使用する
エンティティアダプターには、一般的なセレクターを生成するセレクターファクトリが用意されています。上記の例を取り上げると、次のようにして usersSlice にセレクターを追加できます
// Rename the exports for readability in component usage
export const {
selectById: selectUserById,
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectTotalUsers,
} = usersAdapter.getSelectors((state) => state.users)
その後、次のようにコンポーネントでこれらのセレクターを使用できます
import React from 'react'
import { useSelector } from 'react-redux'
import { selectTotalUsers, selectAllUsers } from './usersSlice'
import styles from './UsersList.module.css'
export function UsersList() {
const count = useSelector(selectTotalUsers)
const users = useSelector(selectAllUsers)
return (
<div>
<div className={styles.row}>
There are <span className={styles.value}>{count}</span> users.{' '}
{count === 0 && `Why don't you fetch some more?`}
</div>
{users.map((user) => (
<div key={user.id}>
<div>{`${user.first_name} ${user.last_name}`}</div>
</div>
))}
</div>
)
}
代替 ID フィールドの指定
デフォルトでは、createEntityAdapter
はデータにentity.id
フィールドに一意のIDがあることを想定しています。データセットがIDを異なるフィールドに格納している場合は、適切なフィールドを返すselectId
引数で渡すことができます。
// In this instance, our user data always has a primary key of `idx`
const userData = {
users: [
{ idx: 1, first_name: 'Test' },
{ idx: 2, first_name: 'Two' },
],
}
// Since our primary key is `idx` and not `id`,
// pass in an ID selector to return that field instead
export const usersAdapter = createEntityAdapter({
selectId: (user) => user.idx,
})
エンティティをソート
createEntityAdapter
はsortComparer
引数を提供し、これを使用して状態内のids
のコレクションをソートできます。これはソート順を保証する必要がある場合や、データがプレソートされてない場合に非常に役立ちます。
// In this instance, our user data always has a primary key of `id`, so we do not need to provide `selectId`.
const userData = {
users: [
{ id: 1, first_name: 'Test' },
{ id: 2, first_name: 'Banana' },
],
}
// Sort by `first_name`. `state.ids` would be ordered as
// `ids: [ 2, 1 ]`, since 'B' comes before 'T'.
// When using the provided `selectAll` selector, the result would be sorted:
// [{ id: 2, first_name: 'Banana' }, { id: 1, first_name: 'Test' }]
export const usersAdapter = createEntityAdapter({
sortComparer: (a, b) => a.first_name.localeCompare(b.first_name),
})
非シリアル化可能データの管理
Reduxのコア使用原則の1つは、状態またはアクションに非シリアル化可能値を入れないことです。
ただし、大多数の規則と同様、例外もいくつかあります。非シリアル化可能データを受け入れるアクションを処理する必要がある場合があります。これは非常にまれに、必要に応じてのみ行う必要があり、これらの非シリアル化可能ペイロードはリデューサーを通じてアプリケーションの状態になるべきではありません。
シリアル化可能性開発チェックミドルウェアは、アクションまたは状態に非シリアル化可能値を検出するたびに自動的に警告します。誤って間違いを犯すことを避けるために、このミドルウェアをアクティブにしておくことをお勧めします。ただし、それらの警告をオフにする必要がある場合は、特定のアクションタイプまたはアクションと状態のフィールドを無視するように構成することで、ミドルウェアをカスタマイズできます。
configureStore({
//...
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['your/action/type'],
// Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}),
})
Redux-Persistと一緒に使用する
Redux-Persistを使用している場合は、特に、そのディスパッチするすべてのアクションタイプを無視する必要があります。
import { configureStore } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
import App from './App'
import rootReducer from './reducers'
const persistConfig = {
key: 'root',
version: 1,
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
let persistor = persistStore(store)
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root'),
)
さらに、persistor.purge()を呼び出すときに消去したい特定のスライスに余分なリデューサーを追加することで、永続化された状態をすべて消去できます。これは、ディスパッチされたログアウトアクションで永続化された状態を消去する場合に特に役立ちます。
import { PURGE } from "redux-persist";
...
extraReducers: (builder) => {
builder.addCase(PURGE, (state) => {
customEntityAdapter.removeAll(state);
});
}
RTK Queryで構成したAPIをブラックリストに含めることも強くお勧めします。APIスライスリデューサーがブラックリストに登録されていない場合、APIキャッシュは自動的に永続化され復元されるため、存在しなくなったコンポーネントからファントムサブスクリプションが発生する可能性があります。このように構成する必要があります。
const persistConfig = {
key: 'root',
version: 1,
storage,
blacklist: [pokemonApi.reducerPath],
}
Redux Toolkit #121: Redux-Persistと一緒にこれを使用する方法?と Redux-Persist #988: 非シリアル化可能値エラーでさらなる議論を参照してください。
React-Redux-Firebaseと一緒に使用する
RRFには、3.xの現在、ほとんどのアクションと状態にタイムスタンプ値が含まれていますが、4.xからは、その動作を改善する可能性のあるPRがあります。
その動作に対応可能な構成は次のようになります。
import { configureStore } from '@reduxjs/toolkit'
import {
getFirebase,
actionTypes as rrfActionTypes,
} from 'react-redux-firebase'
import { constants as rfConstants } from 'redux-firestore'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [
// just ignore every redux-firebase and react-redux-firebase action type
...Object.keys(rfConstants.actionTypes).map(
(type) => `${rfConstants.actionsPrefix}/${type}`,
),
...Object.keys(rrfActionTypes).map(
(type) => `@@reactReduxFirebase/${type}`,
),
],
ignoredPaths: ['firebase', 'firestore'],
},
thunk: {
extraArgument: {
getFirebase,
},
},
}),
})
export default store