原文: https://redux.js.org/style-guide/style-guide
Redux Style Guide
はじめに
この文章は Redux コード記述するための公式スタイルガイドです。
Redux アプリケーションを作成する際の、推奨パターン、ベストプラクティス、推奨されるアプローチをリストアップしています。
Redux のコアライブラリと多くのドキュメントは開発中で固まっていません。多くの使用法があり、一つの正しい解答はありません。
しかし、経験から一部のトピックでは特定のアプローチが他のアプローチよりも効果的である事が示されています。さらに、多くの開発者が意思決定の負担を軽減するための公式ガイダンスの提供を求めています。
それを念頭に、私たちはエラー、終わりのない無駄な議論、アンチパターンを回避するための、推奨リストをまとめました。
チームの好みは様々で、プロジェクトごとに要件も異なるため、全てにおいて当てはまるガイドではありません。
これらの推奨事項に従うことをお勧めしますが、時間をかけて自己の状況を評価し、ニーズに合っているかどうか判断してください。
最後に、このページのインスピレーションとなったVue スタイルガイドページの著者に感謝します。
ルールカテゴリ
私たちはルールを 3 つのカテゴリに分けました。
優先度 A : 必須
これらのルールはエラーの防止に役立つため、コストをかけて学び守ってください。例外はありますが、非常に稀であり、JavaScript と Redux の両方の専門知識を持つ人によってのみ作成されるべきです。
優先度 B : 強く推奨
これらのルールは多くのプロジェクトでコードの読みやすさや開発者エクスペリエンスを向上させることがわかっています。これらに違反してもコードの実行は可能ですが、違反は稀で正当な理由がなければいけません。問題がない限りこれらのルールにしたがってください。
優先度 C : 推奨
複数の同等に優れた選択肢が存在する場合、一貫性を保つために任意の選択が可能です。これらのルールでは許容可能なそれぞれの選択肢について説明し、標準の選択肢を提案しています。
つまり、一貫性があり正当な理由がある限り、独自のコードベースで自由に別の選択をすることができます。正当な理由があればだけどね。
優先度 A ルール : 必須
State を直接変更しない
State の変更は、コンポーネントが適切に再レンダリングされないなど、Redux アプリケーションのバグの最も一般的な原因であり、Redux DevTools のタイムトラベルデバッグも中断します。
Reducer の内部とその他全てのアプリケーションコードで、State の実際の値の変更は常に回避すべきです。
redux-immutable-state-invariantのようなツールを用いて開発中の State の変更を補足し、Immerで予測不能な State の更新を回避します。
Note: 既存の値のコピーを変更してもかまいません。
これは不変更新のロジック記述では普通です。
また、不変更新に Immer を使っている場合、実際には値の変更は行われないため許容されます。Immer は安全に変更を追跡し、内部で不変更新された値を生成します。
Reducer は副作用を持ってはいけない
Reducer 関数はstate
引数とaction
引数のみに依存し、それらの引数に基づいて新しい状態値を計算するだけである必要があります。
あらゆる種類の非同期処理(AJAX 通信、タイムアウト、Promise)、ランダム値の生成(Date.now()
、Math.random()
)、Reducer 外部の変数の変更、Reducer 関数のスコープ外に影響を与える他のコードの実行は禁止されています。
Note: 同じ規則に従っている限り、ライブラリまたはユーティリティ関数からのインポートなど、それ以外の関数で定義されている他の関数を Reducer に呼び出させることは許容されます。
詳細説明
このルールの目的は、Reducer が呼び出されたときに予測どおりに動作することを保証することです。たとえば、タイムトラベルデバッグを実行している場合、「現在の」状態値を生成するために、以前の Action で Reducer 関数が何度も呼び出される場合があります。Reducer に副作用がある場合、これによりデバッグプロセス中にこれらの効果が実行され、アプリケーションが予期しない動作をすることになります。
このルールにはグレーな領域がいくつかあります。厳密に言えば、console.log(state)
のようなコードは副作用ですが、実際にはアプリケーションの動作には影響しません。
State や Actions にシリアル化できない値を入れない
Promise、Symbol、Map/Set、関数、クラスインスタンスなど、非シリアライズな値を State や Action に含めないでください。
これにより Redux DevTools を使ったデバッグなどが期待通り動作することを保証できます。また、UI が期待通り更新されることも保証されます。
例外 : Action が Reducer に到達する前に Middleware によってインターセプト、または停止される場合は、非シリアライズな値を入れても構いません。
redux-thunk
やredux-promise
などのミドルウェアがこの例にあたります。
アプリに対して 1 つの Redux Store を持つこと
標準的な Redux アプリケーションは、アプリケーション全体で使用される Redux Store インスタンスを 1 つだけ持つべきです。
通常はstore.js
などの別のファイルに分離して定義されます。
Store を直接アプリのロジックにインポートしないのが理想です。<Provider>
経由で React コンポーネントツリーに渡すか、redux-thunk
などのミドルウェアを介して間接的に参照する必要があります。まれに、他のロジックファイルにインポートする必要がある場合がありますが、これは最後の手段です。
優先度 B ルール : 強く推奨
Redux のロジック記述に Redux Toolkit を使う
Redux Toolkitは Redux を使う上でおすすめのツールです。
それは、Store の変更を補足し Redux DevTool の拡張機能を有効にしたり、Immer による不変更新ロジックの簡素化など、推奨されるベストプラクティスが組み込まれた関数を持っています。
Redux Toolkit の使用は必須ではなく、必要に応じて自由に他のアプローチをとることができます。しかし Redux Toolkit を利用することで、ロジックをシンプルにし、アプリケーションが適切にセットアップされることを保証します。
不変更新には Immer を使う
手動で不変更新を実装することは、しばしば困難でエラーを起こしやすいです。Immerを使用すると、"mutative"ロジックを使用してより単純な不変の更新を記述でき、開発中の状態をフリーズして、アプリの他の場所で突然変異をキャッチすることもできます。
不変更新ロジックの作成には、できればRedux Toolkitの一部として Immer を使用することをお勧めします。
ファイルを機能フォルダか Ducks で構造化する
Redux 自体はアプリケーションのファイルやフォルダの構造に関与しません。しかし、特定の機能ロジックを同じ場所に配置することで、コードのメンテナンスが楽になります。
このため、ほとんどのアプリケーションでコードの「タイプ」(リデューサー、アクションなど)によってロジックを別々のフォルダに分割するよりも、「機能フォルダ」(単一機能の全てのファイルを同じフォルダ内に配置)または「Ducks」パターン(単一機能の全ての Redux ロジックを一つのファイル内に配置)を使用してファイルを構成することをお勧めします。
詳細説明
フォルダ構造の例は次のようになります。
-
/src
index.tsx
-
/app
store.ts
rootReducer.ts
App.tsx
-
/common
- hooks, generic components, utils, etc
-
/features
-
/todos
todosSlice.ts
Todos.tsx
-
/app
には、他のすべてのフォルダに依存するアプリ全体のセットアップ処理とレイアウトを配置します。
/common
には、汎用的で再利用可能なユーティリティとコンポーネントを配置します。
/features
には、特定の機能に関連するすべての処理を含むフォルダを配置します。
この例でtodosSlice.ts
は、Redux ToolKit の createSlice()
関数への呼び出しを含み、スライス Reducer とアクションクリエーターをエクスポートする「Ducks」スタイルのファイルです。
できるだけ多くのロジックを Reducer に配置する
可能な限り、アクションを準備してディスパッチするコード(クリックハンドラーなど)ではなく、新しい状態を計算するためのロジックを適切な Reducer に配置してください。これにより、実際のアプリロジックを簡単にテストでき、タイムトラベルデバッグをより効果的に使用できるようになり、ミューテーションやバグにつながる可能性のある一般的なミスを回避できます。
新しい状態の一部またはすべてを最初に計算する必要がある有効なケースがありますが(一意の ID の生成など)、それを最小限に抑える必要があります。
詳細説明
Redux は、新しい状態を Reducer で計算されるのか、アクション作成ロジックで計算されるのかを実際には気にしません。たとえば todo アプリの場合、「toggle 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, id: action.payload.id};
})
}
または、最初に新しい配列を計算し、新しい配列全体をアクションに配置します。
// Click handler:
const onTodoClicked = id => {
const newTodos = todos.map(todo => {
if (todo.id !== id) return todo
return { ...todo, id }
})
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 はキー/値の「Slice」によって分割されることが意図されており、各「Slice Reducer」は初期値を提供し、その状態の slice に対する更新を計算する責任があります。
さらに、Slice Reducer は、計算された状態の一部として返される他の値を制御する必要があります。return action.payload
またはreturn {...state, ...action.payload}
のような 「ブラインドスプレッド/リターン」の使用を最小限に抑えます。これらはアクションをディスパッチしたコードに依存してコンテンツを正しくフォーマットし、Reducer はその状態の所有権を効果的に放棄するためです。アクションの内容が正しくない場合、バグが発生する可能性があります。
Note:「スプレッドリターン」は、フォームでデータを編集するようなシナリオでは合理的な選択です。個々のフィールドごとに個別のアクションタイプを記述するのは時間がかかり、ほとんどメリットがありません。
詳細説明
次のような「現在のユーザー」の 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
が正しくフォーマットされたオブジェクトであると完全に想定しています。
ただし、コードの一部が「user」オブジェクトではなく「todo」オブジェクトをアクション内にディスパッチする場合を想像してください:
dispatch({
type: "users/userLoggedIn",
payload: {
id: 42,
text: "Buy milk",
},
});
Reducer は todo を盲目的に返し、これでストアからユーザーを読み取ろうとすると、アプリの残りの部分が壊れる可能性があります。
これは、action.payload
が実際に正しいフィールドを持っているか確認するか、名前で正しいフィールドを読み取ることができるかの検証が Reducer にある場合、少なくとも部分的に修正できます。ただし、これによりコードが追加されるため、安全と引き換えにより多くのコードを書かなければならないことが問題となります。
静的型付けを使用すると、この種のコードがより安全になり、ある程度受け入れやすくなります。action
がPayloadAction<User>
であることを Reducer が知っている場合、return action.payload
を実行するのは安全です。
保存されたデータに基づいて状態スライスに名前をつける
状態の形状は Reducer が所有するで述べたように、Reducer のロジックを分割する標準的なアプローチは状態の「スライス」に基づいています。同様に、combineReducers
は、これらのスライス Reducer をより大きな Reducer 関数に結合するための標準関数です。
combineReducers
に渡されるオブジェクトのキー名は、結果の状態オブジェクトのキーの名前を定義します。これらのキーには、内部に保持されているデータにちなんだ名前を付けてください。また、キー名に「reducer」という単語を使用しないでください。オブジェクトは、{usersReducer: {}, postsReducer: {}}
ではなく、{users: {}, posts: {}}
のようになります。
詳細説明
ES6 オブジェクトリテラルショートハンドを使用すると、オブジェクトのキー名と値を同時に簡単に定義できます:
const data = 42;
const obj = { data };
// same as: {data: data}
combineReducers
は、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,
});
これは少しのタイピングですが、最も理解しやすいコードと状態の定義になります。
Reducer をステートマシンとして扱う
多くの Reducer は「無条件」で記述されています。現在の状態がどうなっているかは関係なく、ディスパッチされたアクションを見て、新しい状態値を計算するのみです。一部のアクションは、アプリの他のロジックによっては、概念的には「有効」ではない場合があるため、これによりバグが発生する可能性があります。たとえば、「リクエストが成功しました」アクションは、状態がすでに「読み込み中」であると計算された場合にのみ新しい値を計算する必要があります。または、「このアイテムを更新」アクションは、「編集中」とマークされたアイテムがある場合にのみディスパッチする必要があります。
これを修正するには、レデューサーを「ステートマシン」として扱います。この場合、現在の状態とディスパッチされたアクションの両方の組み合わせにより、無条件にアクション自体だけでなく、新しい状態値が実際に計算されるかどうかが決定されます。
詳細な説明
有限状態マシンは、常に有限数の「有限状態」のうちの 1 つでなければなりません。たとえば、fetchUserReducer
がある場合、有限状態は次のようになります:
-
"idle"
(フェッチはまだ開始されていません) -
"loading"
(現在ユーザーをフェッチしています) -
"success"
(ユーザーのフェッチに成功しました) -
"failure"
(ユーザーのフェッチに失敗しました)
これらの有限状態を明確にし、不可能な状態を不可能にするには、この有限状態を保持するプロパティを指定します:
const initialUserState = {
status: "idle", // explicit finite state
user: null,
error: null,
};
TypeScript を使用すると、discriminated unionsを使用して、各有限状態を表します。たとえば、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;
}
}
ここで、アクションごとではなく状態ごとに動作を定義しているため、不可能な遷移も防止できます。たとえば、status === LOADING_STATUS
の場合、FETCH_USER
アクションは効果がないはずであり、誤ってエッジケースを導入する代わりに、それを強制できます。
複雑なネスト/リレーショナル状態を正規化する
多くのアプリケーションは、複雑なデータをストアにキャッシュする必要があります。多くの場合、そのデータは API からネストされた形式で受信されるか、データ内のさまざまなエンティティ(ユーザー、投稿、コメントを含むブログなど)間の関係を持っています。
そのデータを「正規化された」形式でストアに保存することをお勧めします。これにより、ID に基づいてアイテムを検索し、ストア内の単一のアイテムを更新することが容易になり、最終的にはより良いパフォーマンスパターンにつながります。
アクションをセッターではなくイベントとしてモデル化する
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 ロジックは、多くの小さな Reducer に分割され、それぞれが独立して状態ツリーの独自の部分を更新し、ルート Reducer 関数を形成するためにすべて一緒に構成されます。特定のアクションがディスパッチされると、すべてか一部の Reducer でハンドリングされるか、もしくはハンドリングされません。
この一環として、可能であれば、多くの Reducer 関数がすべて同じアクションを個別に処理することをお勧めします。実際には、経験から、ほとんどのアクションは通常、単一の Reducer 関数によってのみ処理されることがわかっています。ただし、アクションを「イベント」としてモデル化し、多くの Reducer がそれらのアクションに応答できるようにすることで、通常、アプリケーションのコードベースのスケーリングが向上し、1 つの意味のある更新を実行するために複数のアクションをディスパッチする必要がある回数を最小限に抑えることができます。
多くのアクションを順次ディスパッチしない
より大きな概念的な「トランザクション」を実行するために、連続して多くのアクションをディスパッチしないようにします。これは合法ですが、通常は比較的コストのかかる複数の UI 更新が発生し、中間状態の一部はアプリケーションロジックの他の部分によって無効になる可能性があります。適切なすべての状態を一度に更新する単一の「イベント」タイプのアクションのディスパッチを優先するか、アクションバッチアドオンを使用して、最後に単一の 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 つのサブスクライバー通知のみを生成するより大きな単一のディスパッチに多くのアクションをグループ化するだけの場合があります。追加の例と関連するアドオンへのリンク、「ストア更新イベントの Reduce」に関する FAQ エントリを参照してください。
State をどこに配置すべきか評価する
"Redux の 3 つの原則"は、「アプリケーション全体の状態は単一のツリーに格納される」と述べています。この表現は過度に解釈されています。アプリ全体の文字通りすべての値が Redux ストアに保持されなければならないという意味ではありません。代わりに、あなたがグローバルでアプリ全体と見なすすべての値を見つけるための単一の場所が必要です。「ローカル」の値は通常、代わりに最も近い UI コンポーネントに保持する必要があります。
このため、実際に Redux ストアにどの状態を保持するか、およびコンポーネントの状態を維持するかは、開発者のあなた次第です。これらの経験則を使用して、State の各部分を評価し、それをどこに置くかを決定します。
より多くのコンポーネントを接続してストアからデータを読み取る
Redux ストアにサブスクライブする UI コンポーネントを増やし、より詳細なレベルでデータを読み取ることを推奨します。特定の状態が変化したときにレンダリングする必要のあるコンポーネントが少なくなるため、これは通常、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 オブジェクトを処理するのか、イミュータブルオブジェクトを処理するのかを知っている必要があります
- イミュータブルオブジェクトからプレーン JS オブジェクトへの変換は比較的コストがかかり、常に完全に新しい深いオブジェクト参照を生成します
- ライブラリの継続的なメンテナンスの欠如
Immutable.js を使用する最も強力な理由は、非常に大きなオブジェクト(数万のキー)の高速更新です。ほとんどのアプリケーションは、それほど大きなオブジェクトを処理しません。
全体として、Immutable.js はオーバーヘッドを追加しすぎて、実用的なメリットがほとんどありません。Immer ははるかに良いオプションです。
優先度 C ルール : 推奨
アクションタイプを「domain/eventName」として書く
オリジナルの Redux のドキュメントと例では、アクションタイプを定義するために"ADD_TODO"
や"INCREMENT"
などの「SCREAMING_SNAKE_CASE」規則を一般的に使用していました。これは、定数値を宣言するためのほとんどのプログラミング言語の一般的な規則と一致します。
他のコミュニティは他の規則を採用しており、通常はアクションが関連する「機能」または「ドメイン」、および特定のアクションタイプを示しています。NgRx コミュニティは通常、"[Login Page] Login"
などの"[Domain] Action Type"
のようなパターンを使用します。"domain:action"
のような他のパターンも使用されています。
Redux Toolkit のcreateSlice
関数は現在、"todos/addTodo"
など、"domain/action"
のようなアクションタイプを生成します。今後は、読みやすくするために「ドメイン/アクション」の規則を使用することをお勧めします。
Flux 標準アクション規約を使用してアクションを記述する
オリジナルの「Flux アーキテクチャ」のドキュメントでは、アクションオブジェクトに「type」フィールドを指定するだけで、アクションのフィールドに使用するフィールドの種類や命名規則についてのガイダンスはありませんでした。一貫性を提供するために、Andrew Clark は Redux の開発の初期に"Flux Standard Actions"と呼ばれる規約を作成しました。要約すると、FSA 規約はアクションについて次のように述べています:
- データを常に
payload
フィールドに入れる必要があります - 追加情報のために
meta
フィールドを持つことができます - アクションが何らかの失敗を表すことを示すために、
error
フィールドを持つことができます
Redux エコシステムの多くのライブラリは FSA 規則を採用しており、Redux Toolkit は FSA フォーマットに一致するアクションクリエーターを生成します。
一貫性のために FSA 形式のアクションを使用することをお勧めします。
Note: FSA 仕様では、「エラー」アクションは
error: true
を設定し、アクションの「有効な」フォームと同じアクションタイプを使用する必要があると述べています。実際には、ほとんどの開発者は「成功」と「エラー」のケースに対して別々のアクションタイプを記述します。どちらでもかまいません。
アクションクリエーターを使用する
「アクションクリエーター」機能は、元の「Flux アーキテクチャ」アプローチから始まりました。Redux では、アクションクリエーターは厳密には必要ありません。コンポーネントやその他のロジックは、インラインで記述されたアクションオブジェクトを使用して、dispatch({type: "some/action"})
を常に呼び出すことができます。
ただし、アクションクリエーターを使用すると、特にアクションのコンテンツを入力するために何らかの準備や追加のロジック(一意の ID の生成など)が必要な場合に一貫性が得られます。
アクションのディスパッチには、アクションクリエーターの使用を推奨します。ただし、アクションクリエーターを手動で作成するのではなく、アクションクリエーターとアクションタイプを自動的に生成する Redux Toolkit のcreateSlice
関数を使用することをお勧めします。
非同期ロジックに Redux Thunk を使用する
Redux は拡張可能に設計されており、ミドルウェア API は、さまざまな形式の非同期ロジックを Redux ストアにプラグインできるように特別に作成されました。そうすれば、RxJS のような特定のライブラリがニーズに合わない場合でも、ユーザーがそのライブラリを習得する必要はありません。
これにより、さまざまな Redux 非同期ミドルウェアアドオンが作成され、その結果、どの非同期ミドルウェアを使用すべきかについて混乱と疑問が生じました。
ほとんどの一般的なユースケース(基本的な AJAX データフェッチなど)で十分なので、デフォルトで Redux Thunk ミドルウェアを使用することをお勧めします。さらに、Redux Thunk で async/await
構文を使用すると、読みやすくなります。
キャンセル、デバウンス、特定のアクションがディスパッチされた後のロジックの実行、または「バックグラウンドスレッド」タイプの動作など、本当に複雑な非同期ワークフローがある場合は、Redux-Saga や Redux-Observable などのより強力な非同期ミドルウェアを追加することを検討してください。
コンポーネントの外に複雑なロジックを移動する
伝統的に、コンポーネントの外にできるだけ多くのロジックを保持することを提案してきました。これは、多くのコンポーネントが単にデータを小道具として受け入れ、それに応じて UI を表示する「コンテナ/プレゼンテーション」パターンを奨励したことによるものですが、クラスコンポーネントのライフサイクルメソッドで非同期ロジックを扱うことが維持しにくくなる可能性もあります。
複雑な同期または非同期ロジックをコンポーネントの外に、通常は Redux Thunk に移動することをお勧めします。これは、ロジックがストア状態から読み取る必要がある場合に特に当てはまります。
ただし、React フックを使用すると、コンポーネント内で直接データをフェッチするようなロジックの管理が多少簡単になります。これにより、Redux Thunk の必要性が置き換えられる場合があります。
セレクター関数を使用してストア状態から読み取る
「セレクター関数」は、Redux ストア状態からの値の読み取りをカプセル化し、それらの値からさらにデータを導出するための強力なツールです。さらに、Reselect のようなライブラリを使用すると、入力が変更されたときにのみ結果を再計算するメモ化されたセレクター関数を作成できます。これは、パフォーマンスの最適化の重要な側面です。
可能な限りストアの状態を読み取るためにメモ化されたセレクター関数を使用することを強くお勧めします。これらのセレクターは Reselect で作成することをお勧めします。
ただし、状態のすべてのフィールドにセレクター関数を記述する必要があるとは感じないでください。フィールドがアクセスおよび更新される頻度と、アプリケーションでセレクターが提供する実際の利点の大きさに基づいて、細分性の適切なバランスを見つけます。
フォームの状態を Redux に入れないようにする
ほとんどのフォーム状態は Redux に入れるべきではありません。ほとんどの使用例では、データは真にグローバルではなく、キャッシュされておらず、一度に複数のコンポーネントによって使用されていません。さらに、フォームを Redux に接続すると、多くの場合、変更イベントごとにアクションがディスパッチされるため、パフォーマンスのオーバーヘッドが発生し、実質的なメリットはありません。(おそらく、name: "Mark"
からname: "Mar"
まで 1 文字後ろにタイムトラベルする必要はありません。)
データが最終的に Redux に保存される場合でも、フォーム編集自体をローカルコンポーネントの状態に維持し、ユーザーがフォームに入力完了したときに 1 回だけ、Redux ストアを更新するアクションをディスパッチすることをお勧めします。
編集されたアイテム属性の WYSIWYG ライブプレビューなど、Redux でフォームの状態を維持することが実際に有効なユースケースがあります。ただし、ほとんどの場合、これは必要ありません。