Reduxスタイルガイド
はじめに
これはReduxコード記述のための公式スタイルガイドです。 **推奨パターン、ベストプラクティス、Reduxアプリケーション記述のための推奨アプローチをリストしています。**
ReduxコアライブラリとほとんどのReduxドキュメントは、意見を述べていません。Reduxを使用する方法は多くあり、多くの場合、「正しい」方法は一つだけではありません。
しかし、時間と経験から、いくつかのトピックでは、特定のアプローチが他のアプローチよりも効果的であることが示されています。さらに、多くの開発者から、意思決定の疲労を軽減するための公式ガイドを提供してほしいという要望がありました。
これを踏まえ、 **エラー、無益な議論、アンチパターンを回避するのに役立つ推奨事項をまとめました。** チームの好みは異なり、プロジェクトの要件も異なるため、すべての状況に適合するスタイルガイドはありません。 **これらの推奨事項に従うことをお勧めしますが、ご自身の状況を評価し、ニーズに合っているかどうかを判断する時間を取ってください。**
最後に、このページの参考にさせて頂いたVueスタイルガイドページを執筆されたVueドキュメントの執筆者の方々に感謝申し上げます。
ルールカテゴリー
これらのルールは3つのカテゴリーに分類されています。
優先度A:必須
**これらのルールはエラーを防ぐのに役立つため、必ず学習し、遵守してください。** 例外は存在する可能性がありますが、非常にまれであり、JavaScriptとReduxの両方に精通した人だけが例外を設けるべきです。
優先度B:強く推奨
これらのルールは、ほとんどのプロジェクトで可読性と/または開発者エクスペリエンスを向上させることが分かっています。これらのルールに違反してもコードは実行されますが、違反はまれで、正当な理由が必要です。 **可能な限りこれらのルールに従ってください。**
優先度C:推奨
複数の同等に良いオプションが存在する場合、一貫性を確保するために任意の選択肢を選択できます。これらのルールでは、 **各許容されるオプションを説明し、デフォルトの選択肢を提案します。** つまり、一貫性があり、正当な理由があれば、独自のコードベースで異なる選択肢を選択できます。ただし、正当な理由が必要であることに注意してください!
優先度Aルール:必須
状態の変更をしないでください
状態の変更は、Reduxアプリケーションのバグの最も一般的な原因であり、コンポーネントが正しく再レンダリングされなくなることや、Redux DevToolsでのタイムトラベルデバッグが壊れることなどがあります。 **状態値の実際の変更は常に避けるべきです。** リデューサー内と他のすべてのアプリケーションコードの両方で。
redux-immutable-state-invariant
などのツールを使用して開発中に変更を検出し、Immerを使用して状態更新での偶発的な変更を回避します。
**注記**:既存の値の_コピー_を変更しても問題ありません。これは、不変の更新ロジックを記述する際の通常の処理です。また、不変の更新にImmerライブラリを使用している場合、「変更」ロジックを記述しても問題ありません。Immerは変更を安全に追跡し、内部的に不変に更新された値を生成するためです。
リデューサーは副作用を持つべきではありません
リデューサー関数は、state
およびaction
引数にのみ依存し、これらの引数に基づいて新しい状態値を計算して返す必要があります。 **非同期ロジック(AJAX呼び出し、タイムアウト、プロミス)、ランダム値の生成(Date.now()
、Math.random()
)、リデューサー外部の変数の変更、またはリデューサー関数のスコープ外に影響を与えるその他のコードを実行してはなりません。**
**注記**:ライブラリからのインポートやユーティリティ関数など、リデューサー自身以外で定義された他の関数をリデューサーが呼び出すことは許容されます。ただし、それらの関数も同じルールに従う必要があります。
詳細な説明
このルールの目的は、リデューサーが呼び出されたときに予測可能な動作をすることを保証することです。たとえば、タイムトラベルデバッグを行っている場合、リデューサー関数は以前のアクションで何度も呼び出されて「現在の」状態値が生成される可能性があります。リデューサーに副作用がある場合、デバッグプロセス中にこれらの副作用が実行され、アプリケーションが予期しない動作をすることになります。
このルールにはいくつかのグレーゾーンがあります。厳密に言えば、console.log(state)
などのコードは副作用ですが、実際にはアプリケーションの動作に影響を与えることはありません。
シリアライズできない値を状態またはアクションに入れないでください
**Promise、Symbol、Map/Set、関数、またはクラスインスタンスなどのシリアライズできない値をReduxストアの状態またはディスパッチされたアクションに入れないでください。** これにより、Redux DevToolsによるデバッグなどの機能が期待通りに動作することが保証されます。また、UIが期待通りに更新されることも保証されます。
**例外**:アクションがリデューサーに到達する前にミドルウェアによってインターセプトおよび停止される場合、シリアライズできない値をアクションに入れることができます。
redux-thunk
とredux-promise
は、その例です。
アプリケーションごとにReduxストアを1つだけ使用してください
**標準的なReduxアプリケーションには、アプリケーション全体で使用される単一のReduxストアインスタンスのみが必要です。** 通常、store.js
などの個別のファイルで定義する必要があります。
理想的には、アプリのロジックはストアを直接インポートしません。<Provider>
経由でReactコンポーネントツリーに渡すか、thunkなどのミドルウェア経由で間接的に参照する必要があります。まれに、他のロジックファイルにインポートする必要がある場合がありますが、これは最後の手段としてください。
優先度Bルール:強く推奨
Reduxロジックの記述にはRedux Toolkitを使用してください
**Redux Toolkitは、Reduxを使用するための推奨ツールセットです。** 変更を検出し、Redux DevTools Extensionを有効にするストアの設定、Immerを使用した不変の更新ロジックの簡素化など、推奨されるベストプラクティスを組み込んだ関数があります。
ReduxでRTKを使用する必要はなく、必要に応じて他のアプローチを使用できますが、 **RTKを使用するとロジックが簡素化され、アプリケーションが適切なデフォルト設定で設定されることが保証されます。**
不変の更新の記述にはImmerを使用してください
手動で不変の更新ロジックを記述することは、多くの場合困難でエラーが発生しやすいです。Immerを使用すると、「変更」ロジックを使用してより単純な不変の更新を記述し、開発中に状態をフリーズしてアプリの他の場所での変更を検出できます。 **Redux Toolkitの一部として、不変の更新ロジックの記述にはImmerを使用することをお勧めします。**
機能フォルダと単一ファイルロジックとしてファイルを構造化してください
Redux自体は、アプリケーションのフォルダとファイルの構造を気にしません。しかし、特定の機能のロジックを1か所に配置することで、通常はコードの保守が容易になります。
そのため、 **ほとんどのアプリケーションでは、「機能フォルダ」アプローチを使用してファイルを構造化することをお勧めします。** (同じフォルダ内の機能のすべてのファイル)。特定の機能フォルダ内では、 **その機能のReduxロジックは、単一の「スライス」ファイルとして記述する必要があります。** できれば、Redux ToolkitのcreateSlice
APIを使用します。(これは"ducks"パターンとしても知られています)。古いReduxコードベースでは、「actions」と「reducers」の個別のフォルダを持つ「タイプ別のフォルダ」アプローチが頻繁に使用されていましたが、関連するロジックをまとめることで、そのコードの検索と更新が容易になります。
詳細な説明:フォルダ構造の例
フォルダ構造の例を以下に示します。/src
index.tsx
:Reactコンポーネントツリーをレンダリングするエントリポイントファイル/app
store.ts
:ストア設定rootReducer.ts
:ルートリデューサー(オプション)App.tsx
:ルートReactコンポーネント
/common
:フック、汎用コンポーネント、ユーティリティなど/features
:「機能フォルダ」をすべて含みます/todos
: 単一の機能フォルダtodosSlice.ts
: Redux reducer ロジックと関連するアクションTodos.tsx
: React コンポーネント
/app
: アプリ全体の設定とレイアウトを含み、他のすべてのフォルダに依存しています。
/common
: 真に汎用的で再利用可能なユーティリティとコンポーネントを含みます。
/features
: 特定の機能に関連するすべての機能を含むフォルダがあります。この例では、todosSlice.ts
は「ダック」スタイルのファイルであり、RTK の createSlice()
関数への呼び出しを含み、スライス reducer とアクションクリエイターをエクスポートします。
可能な限り多くのロジックをReducerに記述する
可能な限り、新しい状態を計算するためのロジックの多くを、アクションを準備してディスパッチするコード(クリックハンドラーなど)ではなく、適切な reducer に記述してください。これにより、実際のアプリロジックの大部分が簡単にテスト可能になり、タイムトラベルデバッグをより効果的に使用できるようになり、ミューテーションやバグにつながる一般的なミスを回避するのに役立ちます。
新しい状態の一部またはすべてを最初に計算する必要がある有効なケース(一意の ID を生成するなど)もありますが、これは最小限に抑える必要があります。
詳細な説明
Redux コアは、新しい状態値が reducer で計算されるか、アクション作成ロジックで計算されるかを実際には気にしません。たとえば、todo アプリでは、「todo を切り替える」アクションのロジックは、todo の配列を不変的に更新する必要があります。アクションに todo ID のみを入れ、新しい配列を reducer で計算することは合法です。
// Click handler:
const onTodoClicked = (id) => {
dispatch({type: "todos/toggleTodo", payload: {id}})
}
// Reducer:
case "todos/toggleTodo": {
return state.map(todo => {
if(todo.id !== action.payload.id) return todo;
return {...todo, completed: !todo.completed };
})
}
また、新しい配列を最初に計算し、新しい配列全体をアクションに入れることもできます。
// Click handler:
const onTodoClicked = id => {
const newTodos = todos.map(todo => {
if (todo.id !== id) return todo
return { ...todo, completed: !todo.completed }
})
dispatch({ type: 'todos/toggleTodo', payload: { todos: newTodos } })
}
// Reducer:
case "todos/toggleTodo":
return action.payload.todos;
しかし、いくつかの理由から、reducer でロジックを実行することが好ましいです。
- reducer は純粋関数であるため、常に簡単にテストできます。
const result = reducer(testState, action)
を呼び出して、結果が期待どおりであることをアサートするだけです。したがって、reducer に入れるロジックが多ければ多いほど、簡単にテストできるロジックが多くなります。 - Redux の状態更新は常に不変更新のルールに従う必要があります。ほとんどの Redux ユーザーは、reducer 内でルールに従う必要があることを認識していますが、新しい状態が reducer の外で計算される場合にも必ずこれを行う必要があることは明らかではありません。これにより、偶発的なミューテーションや、Redux ストアから値を読み取ってアクション内に直接渡すといったミスが簡単に起こる可能性があります。すべての状態計算を reducer で行うことで、これらのミスを回避できます。
- Redux Toolkit または Immer を使用している場合、reducer で不変更新ロジックを記述する方がはるかに簡単になり、Immer は状態を凍結して偶発的なミューテーションをキャッチします。
- タイムトラベルデバッグは、ディスパッチされたアクションを「元に戻す」ことができるようにし、その後、何か別のことを行うか、アクションを「やり直す」ことができます。さらに、reducer のホットリロードには、通常、既存のアクションで新しい reducer を再実行することが含まれます。正しいアクションがあるが reducer にバグがある場合、reducer を編集してバグを修正し、ホットリロードすると、すぐに正しい状態が得られます。アクション自体が間違っていた場合、そのアクションがディスパッチされるまでに至った手順を再実行する必要があります。そのため、より多くのロジックが reducer にある方がデバッグが容易になります。
- 最後に、ロジックを reducer に入れることで、アプリケーションコードの他のランダムな部分に散らばっているのではなく、更新ロジックを探す場所がわかります。
Reducerは状態の形状を所有するべきである
Redux ルート状態は、単一のルート reducer 関数によって所有および計算されます。保守性を高めるために、その reducer はキー/値「スライス」によって分割することを目的としており、各「スライス reducer」はその状態のスライスの初期値を提供し、その更新を計算する責任があります。
さらに、スライス reducer は、計算された状態の一部として返される他の値を制御する必要があります。return action.payload
や return {...state, ...action.payload}
のような「ブラインドスプレッド/リターン」の使用を最小限に抑えてください。これらは、アクションをディスパッチしたコードが内容を正しくフォーマットすることに依存しており、reducer は事実上、その状態の外観の所有権を放棄します。アクションの内容が正しくない場合、バグが発生する可能性があります。
注記: 各フィールドごとに別のアクションタイプを記述することが時間がかかり、あまりメリットがないような、フォーム内のデータを編集するシナリオでは、「スプレッドリターン」reducer が妥当な選択肢となる場合があります。
詳細な説明
次のような「現在のユーザー」reducer を想像してください。const initialState = {
firstName: null,
lastName: null,
age: null,
};
export default usersReducer = (state = initialState, action) {
switch(action.type) {
case "users/userLoggedIn": {
return action.payload;
}
default: return state;
}
}
この例では、reducer は action.payload
が正しくフォーマットされたオブジェクトになることを完全に想定しています。
しかし、コードの一部が「ユーザー」オブジェクトではなく「todo」オブジェクトをアクション内にディスパッチした場合を想像してください。
dispatch({
type: 'users/userLoggedIn',
payload: {
id: 42,
text: 'Buy milk'
}
})
reducer は盲目的に todo を返し、ストアからユーザーを読み取ろうとすると、アプリの残りの部分が壊れる可能性が高くなります。
action.payload
に実際に正しいフィールドがあることを確認するための検証チェックを追加するか、名前で正しいフィールドを読み取ろうとすることで、少なくとも部分的に修正できます。ただし、コードが増えるため、安全性を高めるためのコードの増加とのトレードオフの問題になります。
静的型付けを使用すると、この種のコードがより安全になり、ある程度許容できるようになります。reducer が action
が PayloadAction<User>
であることを知っている場合、return action.payload
は安全であるはずです。
保存されたデータに基づいて状態スライスに名前を付ける
Reducerは状態の形状を所有するべきであるで説明したように、reducer ロジックを分割するための標準的なアプローチは、状態の「スライス」に基づいています。それに対応して、combineReducers
は、これらのスライス reducer をより大きな reducer 関数に結合するための標準関数です。
combineReducers
に渡されるオブジェクト内のキー名は、結果の状態オブジェクト内のキー名を定義します。これらのキーには、内部に保持されるデータの名前にし、「reducer」という単語をキー名に使用しないようにしてください。オブジェクトは {users: {}, posts: {}}
のようにする必要があります。{usersReducer: {}, postsReducer: {}}
ではありません。
詳細な説明
オブジェクトリテラル短縮記法を使用すると、オブジェクトでキー名と値を同時に簡単に定義できます。const data = 42
const obj = { data }
// same as: {data: data}
combineReducers
は reducer 関数の完全なオブジェクトを受け入れ、それを使用して同じキー名を持つ状態オブジェクトを生成します。つまり、関数のオブジェクト内のキー名は、状態オブジェクト内のキー名を定義します。
これにより、変数名に「reducer」を使用して reducer をインポートし、オブジェクトリテラル短縮記法を使用して combineReducers
に渡すという一般的な間違いが発生します。
import usersReducer from 'features/users/usersSlice'
const rootReducer = combineReducers({
usersReducer
})
この場合、オブジェクトリテラル短縮記法を使用すると、{usersReducer: usersReducer}
のようなオブジェクトが作成されます。「reducer」が状態キー名に含まれるようになりました。これは冗長で無意味です。
代わりに、内部のデータにのみ関連するキー名を定義します。明確にするために、明示的な key: value
構文を使用することをお勧めします。
import usersReducer from 'features/users/usersSlice'
import postsReducer from 'features/posts/postsSlice'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer
})
多少入力が増えますが、最も理解しやすいコードと状態定義になります。
データ型に基づいて状態構造を整理する(コンポーネントではない)
ルート状態スライスは、UI にある特定のコンポーネントに基づいてではなく、アプリケーションの主要なデータ型または機能領域に基づいて定義および命名する必要があります。これは、Redux ストア内のデータと UI 内のコンポーネントの間に厳密な 1:1 の相関関係がなく、多くのコンポーネントが同じデータにアクセスする必要があるためです。状態ツリーを、アプリのどの部分でもアクセスして、そのコンポーネントに必要な状態の部分だけを読み取ることができる一種のグローバルデータベースと考えてください。
たとえば、ブログアプリでは、誰がログインしているか、著者と投稿に関する情報、そしてアクティブな画面に関する情報を追跡する必要があるかもしれません。適切な状態構造は {auth, posts, users, ui}
のようになる可能性があります。不適切な構造は {loginScreen, usersList, postsList}
のようなものになります。
Reducerをステートマシンとして扱う
多くの Redux reducer は「無条件に」記述されています。ディスパッチされたアクションのみを見て新しい状態値を計算し、現在の状態がどのようなものであるかに基づいてロジックを決定しません。これにより、アプリロジックの残りの部分によっては、一部のアクションが概念的に「有効」ではない場合があるため、バグが発生する可能性があります。たとえば、「リクエストが成功した」アクションは、状態が既に「ロード中」である場合にのみ新しい値が計算されるべきであり、「このアイテムを更新する」アクションは、「編集中」とマークされているアイテムがある場合にのみディスパッチされるべきです。
これを修正するには、reducer を「ステートマシン」として扱い、現在の状態とディスパッチされたアクションの両方の組み合わせによって、新しい状態値が実際に計算されるかどうかを決定します。アクション自体が無条件に決定するのではなく。
詳細な説明
有限状態マシン は、いつでも有限個の「有限状態」のうちの 1 つにのみ存在する必要があるものをモデル化するための便利な方法です。たとえば、fetchUserReducer
がある場合、有限状態は次のようになります。
"idle"
(まだフェッチが開始されていません)"loading"
(現在ユーザーをフェッチしています)"success"
(ユーザーのフェッチに成功しました)"failure"
(ユーザーのフェッチに失敗しました)
これらの有限状態を明確にし、不可能な状態を不可能にするために、この有限状態を保持するプロパティを指定できます。
const initialUserState = {
status: 'idle', // explicit finite state
user: null,
error: null
}
TypeScript を使用すると、識別された共用体 を使用して各有限状態を表すことも簡単になります。たとえば、state.status === 'success'
の場合、state.user
が定義されていることが予想され、state.error
が真ではないことが予想されます。これは型で強制できます。
通常、reducer ロジックは最初にアクションを考慮して記述されます。ステートマシンを使用してロジックをモデル化する場合、最初に状態を考慮することが重要です。「有限状態 reducer」を各状態に対して作成すると、状態ごとの動作をカプセル化するのに役立ちます。
import {
FETCH_USER,
// ...
} from './actions'
const IDLE_STATUS = 'idle';
const LOADING_STATUS = 'loading';
const SUCCESS_STATUS = 'success';
const FAILURE_STATUS = 'failure';
const fetchIdleUserReducer = (state, action) => {
// state.status is "idle"
switch (action.type) {
case FETCH_USER:
return {
...state,
status: LOADING_STATUS
}
}
default:
return state;
}
}
// ... other reducers
const fetchUserReducer = (state, action) => {
switch (state.status) {
case IDLE_STATUS:
return fetchIdleUserReducer(state, action);
case LOADING_STATUS:
return fetchLoadingUserReducer(state, action);
case SUCCESS_STATUS:
return fetchSuccessUserReducer(state, action);
case FAILURE_STATUS:
return fetchFailureUserReducer(state, action);
default:
// this should never be reached
return state;
}
}
状態ごとに動作を定義するようになったため、アクションごとに定義する場合とは異なり、不可能な遷移を防ぐことができます。例えば、FETCH_USER
アクションは、status === LOADING_STATUS
の場合は何の効果もありません。この点を強制することで、意図せずエッジケースが発生するのを防ぐことができます。
複雑なネスト/リレーショナル状態の正規化
多くのアプリケーションでは、ストアに複雑なデータをキャッシュする必要があります。そのデータは、多くの場合、APIからネストされた形式で受信されるか、データ内の異なるエンティティ間に関係があります(ユーザー、投稿、コメントを含むブログなど)。
そのデータをストアに"正規化された形式"で保存することをお勧めします。 これにより、IDに基づいてアイテムを簡単に検索し、ストア内の単一のアイテムを更新することが容易になり、最終的にはパフォーマンスの向上につながります。
状態を最小限に保ち、追加の値を導出する
可能な限り、Reduxストア内の実際のデータは最小限に抑え、必要に応じてその状態から追加の値を導出します。 これには、フィルタリングされたリストの計算や値の合計などが含まれます。例として、TODOアプリでは、TODOオブジェクトの元のリストを状態に保持しますが、状態が更新されるたびに、状態の外部でTODOのフィルタリングされたリストを導出します。同様に、すべてのTODOが完了したかどうか、または残りのTODOの数は、ストアの外でも計算できます。
これにはいくつかの利点があります。
- 実際の状態を読みやすくなります。
- これらの追加の値を計算し、残りのデータと同期させるために必要なロジックが少なくなります。
- 元の状態は参照として残っており、置き換えられません。
データの導出は、多くの場合、「セレクタ」関数で行われ、導出されたデータの計算を行うためのロジックをカプセル化できます。パフォーマンスを向上させるために、これらのセレクタは、reselect
やproxy-memoize
などのライブラリを使用して、以前の結果をキャッシュするようメモ化できます。
アクションをセッターではなくイベントとしてモデル化する
Reduxはaction.type
フィールドの内容を気にしません。定義されていれば構いません。アクションタイプを現在形("users/update"
)、過去形("users/updated"
)、イベントとして記述("upload/progress"
)、または「セッター」として扱う("users/setUserName"
)ことができます。アプリケーションで特定のアクションが何を意味し、それらのアクションをどのようにモデル化するかを決定するのはあなた次第です。
ただし、アクションを「セッター」ではなく「発生したイベントを記述する」ものとして扱うことをお勧めします。 アクションを「イベント」として扱うことで、一般的に、より意味のあるアクション名、ディスパッチされるアクションの総数の減少、およびより意味のあるアクションログ履歴が得られます。「セッター」を記述すると、多くの場合、アクションタイプが多すぎ、ディスパッチが多すぎ、アクションログの意味が薄くなります。
詳細な説明
レストランアプリがあり、誰かがピザとコークを頼んだと想像してみてください。次のようなアクションをディスパッチできます。{ type: "food/orderAdded", payload: {pizza: 1, coke: 1} }
または、次のようにディスパッチできます。
{
type: "orders/setPizzasOrdered",
payload: {
amount: getState().orders.pizza + 1,
}
}
{
type: "orders/setCokesOrdered",
payload: {
amount: getState().orders.coke + 1,
}
}
最初の例は「イベント」です。「ねえ、誰かがピザとコーラを注文したから、何とか処理して」。
2番目の例は「セッター」です。「'注文されたピザ'と'注文されたコーラ'のフィールドがあることを知っています。そして、私はそれらの現在の値をこれらの数値に設定するように指示しています」。
「イベント」のアプローチでは、ディスパッチする必要があるアクションは1つだけで済み、より柔軟です。すでに何枚のピザが注文されていたかは問題ではありません。調理人がいない場合、注文は無視されます。
「セッター」のアプローチでは、クライアントコードは状態の実際の構造、正しい値、そして「トランザクション」を完了するために複数のアクションをディスパッチする必要があることをより詳細に知る必要がありました。
意味のあるアクション名を書く
action.type
フィールドは、主に2つの目的を果たします。
- Reducerロジックは、アクションタイプをチェックして、このアクションを処理して新しい状態を計算する必要があるかどうかを確認します。
- アクションタイプは、あなたが読むためのRedux DevTools履歴ログに表示されます。
アクションを「イベント」としてモデル化するに従って、type
フィールドの実際のコンテンツはRedux自体には関係ありません。しかし、type
値は、開発者であるあなたにとって重要です。アクションは、意味のある、情報が豊富で、記述的なタイプフィールドで記述する必要があります。 理想的には、ディスパッチされたアクションタイプのリストを読み進めて、各アクションの内容を見なくても、アプリケーションで何が起こったかを十分に理解できるはずです。"SET_DATA"
や"UPDATE_STORE"
のような非常に汎用的なアクション名は、何が起こったのかに関する意味のある情報を提供しないため、避けてください。
多くのReducerが同じアクションに応答できるようにする
Redux reducerロジックは、多くの小さなreducerに分割することを目的としています。それぞれが状態ツリーの独自のセクションを独立して更新し、すべてが組み合わされてルートreducer関数になります。特定のアクションがディスパッチされると、すべてのreducer、一部のreducer、またはどのreducerも処理しない場合があります。
これの一部として、可能な限り、多くのreducer関数が同じアクションを個別に処理するようにすることをお勧めします。実際には、ほとんどのアクションは通常、単一のreducer関数によってのみ処理されることが経験的に示されていますが、これは問題ありません。「イベント」としてアクションをモデル化し、多くのreducerがそれらのアクションに応答できるようにすることで、アプリケーションのコードベースをより適切にスケーリングし、意味のある更新を達成するために複数のアクションをディスパッチする必要がある回数を最小限に抑えることができます。
複数のアクションを連続してディスパッチしない
より大きな概念的な「トランザクション」を達成するために、複数のアクションを連続してディスパッチすることを避けてください。 これは合法ですが、通常、比較的コストのかかるUI更新が複数回行われ、アプリケーションロジックの他の部分によっては、中間状態が無効になる可能性があります。一度にすべて適切な状態更新を行う単一の「イベント」タイプのアクションをディスパッチするか、アクションバッチングアドオンを使用して、最後に1回のUI更新のみで複数のアクションをディスパッチすることを検討してください。
詳細な説明
連続してディスパッチできるアクションの数に制限はありません。ただし、ディスパッチされた各アクションは、すべてのストアサブスクリプションコールバック(通常、Reduxに接続されたUIコンポーネントごとに1つ以上)の実行を行い、通常はUIの更新が行われます。Reactイベントハンドラからキューに入れられたUI更新は、通常、単一のReactレンダリングパスにバッチ処理されますが、それらのイベントハンドラの外部からキューに入れられた更新はそうではありません。これには、ほとんどのasync
関数、タイムアウトコールバック、および非Reactコードからのディスパッチが含まれます。これらの状況では、ディスパッチごとに、ディスパッチが完了する前に完全な同期的なReactレンダリングパスが行われ、パフォーマンスが低下します。
さらに、概念的により大きな「トランザクション」スタイルの更新シーケンスの一部である複数ディスパッチは、有効とはみなされない可能性のある中間状態をもたらします。たとえば、アクション"UPDATE_A"
、"UPDATE_B"
、"UPDATE_C"
が連続してディスパッチされ、一部のコードがa
、b
、c
の3つすべてが一緒に更新されることを期待している場合、最初の2つのディスパッチ後の状態は、それらの1つか2つだけが更新されているため、事実上不完全になります。
複数ディスパッチが本当に必要な場合は、何らかの方法で更新をバッチ処理することを検討してください。ユースケースによっては、これはReact自身のレンダリングのバッチ処理(おそらくReact-Reduxのbatch()
を使用)、ストア通知コールバックのデバウンス、または多くのアクションを1つの大きな単一ディスパッチにグループ化して、サブスクライバ通知が1回だけになるようにすることです。「ストア更新イベントの削減」に関するFAQエントリで、追加の例と関連するアドオンへのリンクを参照してください。
各状態の部分がどこに存在すべきかを評価する
"Reduxの3つの原則"では、「アプリケーション全体の状態は単一のツリーに格納されます」と述べられています。この表現は過剰に解釈されてきました。これは、アプリケーション全体の文字通りすべての値をReduxストアに保持する必要があるという意味ではありません。代わりに、グローバルでアプリケーション全体にわたるとあなたが考えるすべての値を見つけるための単一の位置があるはずです。「ローカル」の値は、一般的に、最も近いUIコンポーネントに保持する必要があります。
そのため、開発者として、実際にReduxストアに存在する状態とコンポーネント状態に留まる状態を決定するのはあなた次第です。これらの経験則を使用して、各状態の部分を評価し、どこに存在するべきかを決定してください。
React-Redux Hooks APIを使用する
ReactコンポーネントからReduxストアとやり取りするデフォルトの方法として、React-Redux Hooks API(useSelector
とuseDispatch
)を使用することをお勧めします。従来のconnect
APIはまだ正常に機能し、引き続きサポートされますが、Hooks APIはいくつかの点で一般的に使いやすくなっています。Hooksには間接参照が少なく、記述するコードが少なく、TypeScriptでconnect
よりも簡単に使用できます。
Hooks APIは、パフォーマンスとデータフローの点でconnect
とは異なるトレードオフを導入しますが、現在ではデフォルトとして推奨しています。
詳細な説明
従来のconnect
APIは高階コンポーネントです。ストアを購読し、独自のコンポーネントをレンダリングし、ストアとアクションクリエイターからのデータをプロップとして渡す新しいラッパーコンポーネントを生成します。
これは意図的な間接レベルであり、「プレゼンテーション」スタイルのコンポーネントを記述できます。これらのコンポーネントは、すべての値をプロップとして受け取り、Reduxに依存することなく記述できます。
Hooksの導入により、ほとんどのReact開発者がコンポーネントを記述する方法が変わりました。「コンテナ/プレゼンテーション」の概念はまだ有効ですが、Hooksは、適切なHookを呼び出すことで、独自のデータを内部的に要求する責任を負うコンポーネントを記述するように促します。これにより、コンポーネントとロジックの記述とテスト方法が異なります。
connect
の間接参照は、データフローを追跡することを一部のユーザーにとって困難にしてきました。さらに、connect
の複雑さにより、複数のオーバーロード、オプションのパラメーター、mapState
/ mapDispatch
/ 親コンポーネントからの props のマージ、そしてアクションクリエーターとthunkのバインディングのために、TypeScriptで正しく記述することが非常に困難になっています。
useSelector
と useDispatch
は間接参照を排除するため、独自のコンポーネントがReduxとどのようにやり取りしているかがはるかに明確になります。useSelector
は単一のセレクターのみを受け入れるため、TypeScript で定義するのがはるかに容易になり、useDispatch
についても同じことが言えます。
詳細については、ReduxのメンテナーであるMark Erikson氏によるフックとHOCのトレードオフに関する投稿とカンファレンストークをご覧ください。
コンポーネントの最適化とまれなエッジケースの処理方法については、React-ReduxフックAPIドキュメントも参照してください。
ストアからデータを読み取るためのより多くのコンポーネントの接続
より多くのUIコンポーネントをReduxストアに購読し、より詳細なレベルでデータを読み取ることを優先します。これにより、特定の状態の変化時にレンダリングが必要となるコンポーネントが少なくなるため、通常はUIのパフォーマンスが向上します。
たとえば、<UserList>
コンポーネントを接続してユーザーの配列全体を読み取るのではなく、<UserList>
ですべてのユーザーIDのリストを取得し、リストアイテムを <UserListItem userId={userId}>
としてレンダリングし、<UserListItem>
を接続してストアから独自のユーザーエントリを抽出します。
これは、React-Reduxのconnect()
APIとuseSelector()
フックの両方に適用されます。
connect
で mapDispatch
のオブジェクト簡略表記を使用する
connect
への mapDispatch
引数は、引数として dispatch
を受け取る関数として、またはアクションクリエーターを含むオブジェクトとして定義できます。mapDispatch
の「オブジェクト簡略表記」形式を常に使用することをお勧めします。コードが大幅に簡素化されます。mapDispatch
を関数として記述する必要があるケースはほとんどありません。
関数コンポーネントで useSelector
を複数回呼び出す
useSelector
フックを使用してデータを取得する場合、オブジェクト内で複数の結果を返す単一の大きなuseSelector
呼び出しを行うのではなく、useSelector
を複数回呼び出して少量のデータを取得することを優先します。mapState
とは異なり、useSelector
はオブジェクトを返す必要はなく、セレクターがより小さな値を読み取ることで、特定の状態の変化によってこのコンポーネントがレンダリングされる可能性が低くなります。
ただし、適切な粒度を見つけるようにしてください。単一のコンポーネントで状態のスライス内のすべてのフィールドが必要な場合は、各フィールドの個別のセレクターではなく、そのスライス全体を返す1つのuseSelector
を記述するだけです。
静的型付けを使用する
プレーンなJavaScriptではなく、TypeScriptやFlowなどの静的型システムを使用してください。型システムは多くの一般的な間違いをキャッチし、コードのドキュメントを改善し、最終的には長期的な保守性を向上させます。ReduxとReact-ReduxはもともとプレーンなJSを念頭に置いて設計されましたが、どちらもTSとFlowとよく連携します。Redux Toolkitは特にTSで記述されており、追加の型宣言を最小限に抑えて優れた型安全性を提供するように設計されています。
デバッグにRedux DevTools拡張機能を使用する
Redux DevTools拡張機能を使用してデバッグを有効にするようにReduxストアを構成してください。これにより、次のような情報を見ることができます。
- ディスパッチされたアクションの履歴ログ
- 各アクションの内容
- アクションがディスパッチされた後の最終的な状態
- アクション後の状態の差分
- アクションが実際にディスパッチされたコードを示す関数スタックトレース
さらに、DevToolsを使用すると、「タイムトラベルデバッグ」を実行して、アクション履歴を前後にステップして、異なる時点でのアプリケーションの状態とUI全体を確認できます。
Reduxはこの種のデバッグを可能にするために特別に設計されており、DevToolsはReduxを使用する最も強力な理由の1つです。.
状態にプレーンなJavaScriptオブジェクトを使用する
Immutable.jsなどの特殊なライブラリではなく、状態ツリーにプレーンなJavaScriptオブジェクトと配列を使用することを優先します。Immutable.jsを使用することにはいくつかの潜在的な利点がありますが、簡単な参照比較など、一般的に述べられている目標のほとんどは、一般的に不変の更新の特性であり、特定のライブラリを必要としません。これにより、バンドルサイズが小さくなり、データ型の変換による複雑さが軽減されます。
前述のように、特にRedux Toolkitの一部として、不変の更新ロジックを簡素化したい場合は、Immerを使用することを強くお勧めします。
詳細な説明
Immutable.jsは、当初からReduxアプリで頻繁に使用されてきました。Immutable.jsの使用について述べられている一般的な理由はいくつかあります。- 安価な参照比較によるパフォーマンスの向上
- 特殊なデータ構造による更新によるパフォーマンスの向上
- 偶発的な変更の防止
setIn()
などのAPIによるネストされた更新が容易
これらの理由にはいくつかの有効な側面がありますが、実際には、利点は述べられているほどではなく、使用することには複数の欠点があります。
- 安価な参照比較は、Immutable.jsだけでなく、あらゆる不変の更新の特性です。
- Immer(事故を起こしやすい手動コピーロジックを排除し、開発時にデフォルトで状態をディープフリーズします)または
redux-immutable-state-invariant
(状態の変更をチェックします)など、他のメカニズムを使用して、偶発的な変更を防ぐことができます。 - Immerを使用すると、全体的な更新ロジックが簡素化され、
setIn()
の必要性がなくなります。 - Immutable.jsのバンドルサイズは非常に大きいです。
- APIは非常に複雑です。
- APIはアプリケーションのコードに「感染」します。すべてのロジックは、プレーンなJSオブジェクトとImmutableオブジェクトのどちらを扱っているかを認識する必要があります。
- ImmutableオブジェクトからプレーンなJSオブジェクトへの変換は比較的コストが高く、常に完全に新しい深いオブジェクト参照が生成されます。
- ライブラリへの継続的なメンテナンスの欠如
Immutable.jsを使用する最も強力な残りの理由は、非常に大きなオブジェクト(数万個のキー)の高速な更新です。ほとんどのアプリケーションは、それほど大きなオブジェクトを扱うことはありません。
全体として、Immutable.jsは、あまり実用的なメリットがないのに、多すぎるオーバーヘッドを追加します。Immerははるかに優れた選択肢です。
優先事項Cルール:推奨事項
アクションタイプをdomain/eventName
として記述する
元のReduxドキュメントと例では、一般的に"ADD_TODO"
や"INCREMENT"
など、アクションタイプの定義に「SCREAMING_SNAKE_CASE」規則が使用されていました。これは、定数値を宣言するためのほとんどのプログラミング言語の一般的な規則と一致します。欠点は、大文字の文字列が読みづらいことです。
他のコミュニティでは、通常、アクションが関連付けられている「機能」または「ドメイン」と、特定のアクションタイプを示す何らかの方法で、他の規則を採用しています。NgRxコミュニティは、一般的に"[Domain] Action Type"
のようなパターン(例:"[Login Page] Login"
)を使用しています。"domain:action"
のような他のパターンも使用されてきました。
Redux ToolkitのcreateSlice
関数は、現在、"todos/addTodo"
など、"domain/action"
のように見えるアクションタイプを生成します。今後、可読性のために"domain/action"
規則を使用することをお勧めします。
Flux標準アクション規則を使用してアクションを作成する
元の「Fluxアーキテクチャ」ドキュメントでは、アクションオブジェクトにtype
フィールドを含めることのみが指定されており、アクションのフィールドに使用されるフィールドの種類や命名規則については、それ以上のガイダンスはありませんでした。一貫性を提供するために、Andrew ClarkはReduxの開発の初期に"Flux標準アクション"と呼ばれる規則を作成しました。要約すると、FSA規則では、アクションは次のようになります。
- 常にデータを
payload
フィールドに入れる必要があります。 - 追加情報のために
meta
フィールドを持つ場合があります。 - アクションが何らかの失敗を表していることを示す
error
フィールドを持つ場合があります。
Reduxエコシステムの多くのライブラリはFSA規則を採用しており、Redux ToolkitはFSA形式に一致するアクションクリエーターを生成します。
一貫性のためにFSA形式のアクションを使用することを優先します。.
注記:FSA仕様では、「error」アクションは
error: true
を設定し、「有効な」アクション形式と同じアクションタイプを使用する必要があります。実際には、ほとんどの開発者は「成功」と「エラー」のケースに別々のアクションタイプを記述します。どちらでも受け入れられます。
アクションクリエーターを使用する
アクションクリエーター関数は、元の「Fluxアーキテクチャ」アプローチから始まりました。Reduxでは、アクションクリエーターは厳密には必要ありません。コンポーネントやその他のロジックは、常にインラインで記述されたアクションオブジェクトを使用してdispatch({type: "some/action"})
を呼び出すことができます。
ただし、アクションクリエーターを使用すると、特に何らかの準備または追加のロジックが必要な場合(一意のIDの生成など)、アクションの内容を記入する必要がある場合に一貫性が得られます。
すべてのアクションのディスパッチには、アクションクリエーターを使用することを優先します。ただし、手動でアクションクリエーターを作成するのではなく、アクションクリエーターとアクションタイプを自動的に生成するRedux ToolkitのcreateSlice
関数を使用することをお勧めします。
データ取得にRTK Queryを使用する
実際には、典型的なReduxアプリでの副作用の最も一般的なユースケースは、サーバーからのデータの取得とキャッシュです。
そのため、Reduxアプリでのデータの取得とキャッシュのデフォルトのアプローチとしてRTK Queryを使用することをお勧めします。RTK Queryは、必要に応じてサーバーからデータを取得し、キャッシュし、要求を重複除去し、コンポーネントを更新するロジックなどを正しく管理するように設計されています。ほとんどの場合、手動でデータ取得ロジックを作成することはお勧めしません。
他の非同期ロジックにはThunkとリスナーを使用する
Reduxは拡張性を考慮して設計されており、ミドルウェアAPIは特に、様々な形式の非同期ロジックをReduxストアにプラグインできるように作成されました。これにより、ユーザーはニーズに合わない場合、RxJSのような特定のライブラリを学習する必要がなくなります。
この結果、多種多様なRedux非同期ミドルウェアアドオンが作成され、どの非同期ミドルウェアを使用すべきかについての混乱と疑問が生じています。
命令型のロジック(`dispatch`や`getState`へのアクセスが必要な複雑な同期ロジック、および中程度の複雑さの非同期ロジックなど、コンポーネントからロジックを移動する場合など)には、Redux thunkミドルウェアの使用をお勧めします。
ディスパッチされたアクションや状態の変化に対応する必要がある「リアクティブ」なロジック(長時間にわたる非同期ワークフローや「バックグラウンドスレッド」タイプの動作など)には、RTKの「リスナー」ミドルウェアの使用をお勧めします。
ほとんどの場合、特に非同期データフェッチにおいては、より複雑なRedux-SagaやRedux-Observableライブラリの使用は推奨しません。これらのライブラリは、他のツールでは処理できないユースケースの場合にのみ使用してください。
コンポーネントの外に複雑なロジックを移動する
従来、可能な限り多くのロジックをコンポーネントの外に保持することを推奨してきました。これは、多くのコンポーネントが単にプロップとしてデータを受け取り、それに応じてUIを表示する「コンテナ/プレゼンテーション」パターンを推奨したためですが、クラスコンポーネントのライフサイクルメソッドで非同期ロジックを処理することは、保守が困難になる可能性があるためでもあります。
複雑な同期または非同期ロジックは、通常thunkを使用してコンポーネントの外に移動することを依然として推奨します。これは、ロジックがストアの状態から読み取る必要がある場合に特に当てはまります。
ただし、Reactフックを使用すると、データフェッチなどのロジックをコンポーネント内で直接管理することが多少容易になり、場合によってはthunkの必要性を置き換える可能性があります。
セレクター関数を使用してストアの状態から読み取る
「セレクター関数」は、Reduxストアの状態から値を読み取り、それらの値からさらにデータを導出するための強力なツールです。さらに、Reselectなどのライブラリを使用すると、入力が変更された場合にのみ結果を再計算するメモ化されたセレクター関数を作成できます。これは、パフォーマンスを最適化するための重要な側面です。
可能な限り、メモ化されたセレクター関数を使用してストアの状態を読み取ることを強くお勧めします。Reselectを使用してこれらのセレクターを作成することをお勧めします。
ただし、状態のすべてのフィールドに対してセレクター関数を作成する必要はないと考えてください。フィールドのアクセス頻度と更新頻度、アプリケーションでセレクターが実際にどの程度のメリットを提供しているかに基づいて、粒度について適切なバランスを見つけましょう。
セレクター関数の名前を`selectThing`とする
セレクター関数の名前には、`select`という単語と、選択される値の説明を組み合わせたプレフィックスを付けることをお勧めします。例としては、`selectTodos`、`selectVisibleTodos`、`selectTodoById`などがあります。
フォームの状態をReduxに格納しない
ほとんどのフォームの状態はReduxに格納するべきではありません。ほとんどのユースケースでは、データは真にグローバルではなく、キャッシュされておらず、複数のコンポーネントで一度に使用されていません。さらに、フォームをReduxに接続すると、多くの場合、すべての変更イベントでアクションをディスパッチする必要があり、パフォーマンスのオーバーヘッドが発生し、実際的なメリットはありません。(おそらく、`name: "Mark"`から`name: "Mar"`まで1文字ずつ時間を遡る必要はありません。)
データが最終的にReduxに格納される場合でも、フォームの編集自体はローカルコンポーネントの状態に保持し、ユーザーがフォームを完了した後にのみ、Reduxストアを更新するアクションをディスパッチすることを優先します。
編集されたアイテム属性のWYSIWYGライブプレビューなど、フォームの状態をReduxに保持することが実際的な意味を持つユースケースもありますが、ほとんどの場合、これは必要ありません。