セレクターを使用したデータの派生
- 優れたReduxアーキテクチャが状態を最小限に保ち、追加データを派生させる理由
- セレクター関数を使用してデータを派生させ、ルックアップをカプセル化する原則
- Reselectライブラリを使用して、最適化のためにメモ化されたセレクターを記述する方法
- Reselectを使用するための高度なテクニック
- セレクターを作成するための追加のツールとライブラリ
- セレクターを記述するためのベストプラクティス
データの派生
Reduxアプリでは、Reduxの状態を最小限に保ち、可能な限りその状態から追加の値を派生させることを特にお勧めします。
これには、フィルターされたリストの計算や値の合計などが含まれます。たとえば、TODOアプリは状態にTODOオブジェクトの元のリストを保持しますが、状態が更新されるたびに状態外でTODOのフィルターされたリストを派生させます。同様に、すべてのTODOが完了したかどうかのチェック、または残りのTODOの数もストア外で計算できます。
これには、いくつかの利点があります
- 実際の状態が読みやすくなる
- これらの追加の値を計算し、それらをデータの残りの部分と同期させるために必要なロジックが少なくなる
- 元の状態は参照として残っており、置き換えられていない
これはReactの状態にも当てはまります。多くの場合、ユーザーは状態値の変更を待つ `useEffect` フックを定義し、 `setAllCompleted(allCompleted)` などの派生値で状態を設定しようとしました。代わりに、その値はレンダリングプロセス中に派生させて直接使用できるため、値を状態に保存する必要はありません。
function TodoList() {
const [todos, setTodos] = useState([])
// Derive the data while rendering
const allTodosCompleted = todos.every(todo => todo.completed)
// render with this value
}
セレクターを使用した派生データの計算
典型的なReduxアプリケーションでは、データの派生ロジックは通常、**_セレクター_**と呼ばれる関数として記述されます。
セレクターは、主に状態から特定の値を検索するためのロジック、実際に値を派生させるためのロジック、不要な再計算を回避することによるパフォーマンスの向上に使用されます。
すべての状態ルックアップにセレクターを使用する**必要はありません**が、セレクターは標準的なパターンであり、広く使用されています。
基本的なセレクターの概念
「セレクター関数」とは、Reduxストアの状態(または状態の一部)を引数として受け取り、その状態に基づくデータを返す関数です。
**セレクターは特別なライブラリを使用して記述する必要はありません**。また、アロー関数として記述するか、 `function` キーワードとして記述するかは問題ありません。たとえば、これらはすべて有効なセレクター関数です
// Arrow function, direct lookup
const selectEntities = state => state.entities
// Function declaration, mapping over an array to derive values
function selectItemIds(state) {
return state.items.map(item => item.id)
}
// Function declaration, encapsulating a deep lookup
function selectSomeSpecificField(state) {
return state.some.deeply.nested.field
}
// Arrow function, deriving values from an array
const selectItemsWhoseNamesStartWith = (items, namePrefix) =>
items.filter(item => item.name.startsWith(namePrefix))
セレクター関数には、任意の名前を付けることができます。ただし、**セレクター関数の名前には、 `select` という単語と選択される値の説明を組み合わせたプレフィックスを付けることをお勧めします**。この典型的な例としては、** `selectTodoById` **、** `selectFilteredTodos` **、** `selectVisibleTodos` **などがあります。
React-Reduxの `useSelector` フックを使用したことがある場合は、セレクター関数の基本的な考え方( `useSelector` に渡す関数はセレクターでなければならない)についてすでに理解しているでしょう。
function TodoList() {
// This anonymous arrow function is a selector!
const todos = useSelector(state => state.todos)
}
セレクター関数は、通常、Reduxアプリケーションの2つの異なる部分で定義されます
- スライスファイル内、reducerロジックと並べて
- コンポーネントファイル内、コンポーネント外、または `useSelector` 呼び出し内にインラインで
セレクター関数は、Reduxルート状態値全体にアクセスできる場所であればどこでも使用できます。これには、 `useSelector` フック、 `connect` の `mapState` 関数、ミドルウェア、サンク、サーガが含まれます。たとえば、サンクとミドルウェアは `getState` 引数にアクセスできるため、そこでセレクターを呼び出すことができます
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)
if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}
スライスreducerはRedux状態の自身のsliceにのみアクセスでき、ほとんどのセレクターは引数としてReduxルート状態全体を受け取ることを期待しているため、通常、reducer内でセレクターを使用することはできません。
セレクターによる状態シェイプのカプセル化
セレクター関数を使用する最初の理由は、Redux状態シェイプを扱う際のカプセル化と再利用性のためです。
`useSelector` フックの1つがRedux状態の一部に対して非常に具体的なルックアップを行うとしましょう
const data = useSelector(state => state.some.deeply.nested.field)
これは正当なコードであり、正常に動作します。ただし、アーキテクチャ的には最良のアイデアではないかもしれません。そのフィールドにアクセスする必要があるコンポーネントが複数あるとします。その状態が配置されている場所を変更する必要がある場合はどうなりますか?その値を参照するすべての `useSelector` フックを変更する必要があります。アクションの作成に関する詳細をカプセル化するためにアクションクリエーターを使用することをお勧めするように、特定の状態が配置されている場所の知識をカプセル化するために、再利用可能なセレクターを定義することをお勧めします。その後、コードベース内の必要な場所で、そのセレクター関数を複数回使用できます。
理想的には、reducer関数とセレクターのみが正確な状態構造を知る必要があるため、状態が配置されている場所を変更する場合、それら2つのロジックのみを更新する必要があります.
このため、再利用可能なセレクターは、常にコンポーネント内で定義するのではなく、スライスファイル内で直接定義することをお勧めします。
セレクターの一般的な説明の1つは、セレクターは**「状態へのクエリ」**のようなものであるということです。必要なデータをクエリがどのように取得したかについては気にせず、データのリクエストを行い、結果が返されたことのみが重要です。
メモ化によるセレクターの最適化
セレクター関数は、多くの場合、比較的「コストのかかる」計算を実行したり、新しいオブジェクトおよび配列参照である派生値を作成する必要があります。これは、いくつかの理由から、アプリケーションのパフォーマンスにとって懸念事項となる可能性があります
- `useSelector` または `mapState` で使用されるセレクターは、Reduxルート状態のどのセクションが実際に更新されたかに関係なく、ディスパッチされたすべてのアクションの後に再実行されます。入力状態セクションが変更されていない場合にコストのかかる計算を再実行することはCPU時間の無駄であり、ほとんどの場合、入力は変更されない可能性が非常に高くなります。
- `useSelector` と `mapState` は、コンポーネントを再レンダリングする必要があるかどうかを判断するために、戻り値の `===` 参照等価性チェックに依存しています。セレクターが常に新しい参照を返す場合、派生データが事実上前回と同じであっても、コンポーネントは強制的に再レンダリングされます。これは、新しい配列参照を返す `map()` や `filter()` などの配列操作で特に一般的です。
例として、このコンポーネントは、 `useSelector` 呼び出しが常に新しい配列参照を返すため、記述が不適切です。つまり、入力 `state.todos` スライスが変更されていない場合でも、コンポーネントはディスパッチされたすべてのアクションの後に再レンダリングされます
function TodoList() {
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const completedTodos = useSelector(state =>
state.todos.map(todo => todo.completed)
)
}
別の例は、データを変換するために「コストのかかる」作業を行う必要があるコンポーネントです
function ExampleComplexComponent() {
const data = useSelector(state => {
const initialData = state.data
const filteredData = expensiveFiltering(initialData)
const sortedData = expensiveSorting(filteredData)
const transformedData = expensiveTransformation(sortedData)
return transformedData
})
}
同様に、この「コストのかかる」ロジックは、ディスパッチされたすべてのアクションの後に再実行されます。新しい参照が作成される可能性が高いだけでなく、 `state.data` が実際に変更されない限り、実行する必要のない作業です。
このため、同じ入力が渡された場合に結果の再計算を回避できる、最適化されたセレクターを記述する方法が必要です。ここで、**_メモ化_**の概念が登場します。
**メモ化はキャッシングの一種です**。関数の入力を追跡し、入力と結果を後で参照するために保存することが含まれます。関数が以前と同じ入力で呼び出された場合、関数は実際の作業をスキップし、前回それらの入力値を受け取ったときに生成したのと同じ結果を返すことができます。これにより、入力が変更された場合にのみ作業を行い、入力が同じであれば常に同じ結果参照を返すことで、パフォーマンスが最適化されます。
次に、メモ化されたセレクターを記述するためのオプションをいくつか見ていきます。
Reselectを使用したメモ化されたセレクターの記述
Reduxエコシステムでは、従来、**Reselect**と呼ばれるライブラリを使用して、メモ化されたセレクター関数を作成していました。他にも同様のライブラリや、Reselectのバリエーションやラッパーが複数あります。これらについては後で説明します。
`createSelector` の概要
Reselectは、 `createSelector` と呼ばれる関数を備えており、メモ化されたセレクターを生成します。 `createSelector` は、1つ以上の「入力セレクター」関数と「出力セレクター」関数を受け取り、使用する新しいセレクター関数を返します。
`createSelector` は、公式のRedux Toolkitパッケージの一部として含まれており、使いやすくするために再エクスポートされています。
`createSelector` は、複数の入力セレクターを受け取ることができ、それらは個別の引数として、または配列として提供できます。すべての入力セレクターからの結果は、出力セレクターに個別の引数として提供されます
const selectA = state => state.a
const selectB = state => state.b
const selectC = state => state.c
const selectABC = createSelector([selectA, selectB, selectC], (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
// Call the selector function and get a result
const abc = selectABC(state)
// could also be written as separate arguments, and works exactly the same
const selectABC2 = createSelector(selectA, selectB, selectC, (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
セレクタを呼び出すと、Reselect は指定されたすべての引数を使用して入力セレクタを実行し、返された値を確認します。いずれかの結果が以前と `===` で異なる場合、出力セレクタを再実行し、それらの結果を引数として渡します。すべての結果が前回と同じであれば、出力セレクタの再実行をスキップし、キャッシュされた最終結果を返します。
これは、**「入力セレクタ」は通常、値を抽出して返すだけで、「出力セレクタ」は変換作業を行うべきである**ことを意味します。
よくある間違いは、値を抽出したり、何らかの派生を行ったりする「入力セレクタ」と、その結果を返すだけの「出力セレクタ」を作成することです。
// ❌ BROKEN: this will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
state => state.todos,
todos => todos
)
**入力を返すだけの「出力セレクタ」は誤りです!** 出力セレクタは常に変換ロジックを持つべきです。
同様に、メモ化されたセレクタは、入力として `state => state` を使用してはいけません! これは、セレクタが常に再計算を強制することになります。
典型的な Reselect の使用法では、トップレベルの「入力セレクタ」をプレーン関数として記述し、`createSelector` を使用してネストされた値を検索するメモ化されたセレクタを作成します。
const state = {
a: {
first: 5
},
b: 10
}
const selectA = state => state.a
const selectB = state => state.b
const selectA1 = createSelector([selectA], a => a.first)
const selectResult = createSelector([selectA1, selectB], (a1, b) => {
console.log('Output selector running')
return a1 + b
})
const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15
const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15
`selectResult` を 2 回目に呼び出したとき、「出力セレクタ」は実行されませんでした。 `selectA1` と `selectB` の結果が最初の呼び出しと同じであったため、`selectResult` は最初の呼び出しからメモ化された結果を返すことができました。
`createSelector` の動作
デフォルトでは、**`createSelector` は最新のパラメータセットのみをメモ化する**ことに注意することが重要です。 つまり、異なる入力でセレクタを繰り返し呼び出すと、結果は返されますが、結果を生成するために出力セレクタを再実行し続ける必要があります。
const a = someSelector(state, 1) // first call, not memoized
const b = someSelector(state, 1) // same inputs, memoized
const c = someSelector(state, 2) // different inputs, not memoized
const d = someSelector(state, 1) // different inputs from last time, not memoized
また、セレクタに複数の引数を渡すことができます。 Reselect は、これらの正確な入力ですべての入力セレクタを呼び出します。
const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId
const selectItemById = createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
)
const item = selectItemById(state, 42)
/*
Internally, Reselect does something like this:
const firstArg = selectItems(state, 42);
const secondArg = selectItemId(state, 42);
const result = outputSelector(firstArg, secondArg);
return result;
*/
このため、**提供するすべての「入力セレクタ」が同じタイプのパラメータを受け入れることが重要です**。 そうしないと、セレクタが壊れます。
const selectItems = state => state.items
// expects a number as the second argument
const selectItemId = (state, itemId) => itemId
// expects an object as the second argument
const selectOtherField = (state, someObject) => someObject.someField
const selectItemById = createSelector(
[selectItems, selectItemId, selectOtherField],
(items, itemId, someField) => items[itemId]
)
この例では、`selectItemId` は 2 番目の引数が単純な値であることを想定していますが、`selectOtherField` は 2 番目の引数がオブジェクトであることを想定しています。 `selectItemById(state, 42)` を呼び出すと、`selectOtherField` は `42.someField` にアクセスしようとするため、壊れます。
Reselect の使用パターンと制限
セレクタのネスト
`createSelector` で生成されたセレクタを取得し、それらを他のセレクタの入力として使用することも可能です。 この例では、`selectCompletedTodos` セレクタは `selectCompletedTodoDescriptions` への入力として使用されます。
const selectTodos = state => state.todos
const selectCompletedTodos = createSelector([selectTodos], todos =>
todos.filter(todo => todo.completed)
)
const selectCompletedTodoDescriptions = createSelector(
[selectCompletedTodos],
completedTodos => completedTodos.map(todo => todo.text)
)
入力パラメータの受け渡し
Reselect によって生成されたセレクタ関数は、必要な数の引数で呼び出すことができます:`selectThings(a, b, c, d, e)`。 ただし、出力の再実行にとって重要なのは、引数の数や、引数自体が新しい参照に変更されたかどうかではありません。 代わりに、定義された「入力セレクタ」と、*それらの*結果が変更されたかどうかが重要です。 同様に、「出力セレクタ」の引数は、入力セレクタが返す内容のみに基づいています。
これは、出力セレクタに追加パラメータを渡したい場合は、元のセレクタ引数からそれらの値を抽出する入力セレクタを定義する必要があることを意味します。
const selectItemsByCategory = createSelector(
[
// Usual first input - extract value from `state`
state => state.items,
// Take the second arg, `category`, and forward to the output selector
(state, category) => category
],
// Output selector gets (`items, category)` as args
(items, category) => items.filter(item => item.category === category)
)
その後、セレクタを次のように使用できます。
const electronicItems = selectItemsByCategory(state, "electronics");
一貫性を保つために、セレクタに追加パラメータを `selectThings(state, otherArgs)` などの単一オブジェクトとして渡し、`otherArgs` オブジェクトから値を抽出することを検討してください。
セレクタファクトリ
**`createSelector` のデフォルトのキャッシュサイズは 1 であり、これはセレクタの各ユニークインスタンスごとです**。 これにより、単一のセレクタ関数を異なる入力で複数の場所で再利用する必要がある場合に問題が発生します。
1 つの選択肢は、「セレクタファクトリ」を作成することです。これは、`createSelector()` を実行し、呼び出されるたびに新しいユニークなセレクタインスタンスを生成する関数です。
const makeSelectItemsByCategory = () => {
const selectItemsByCategory = createSelector(
[state => state.items, (state, category) => category],
(items, category) => items.filter(item => item.category === category)
)
return selectItemsByCategory
}
これは、複数の類似した UI コンポーネントが props に基づいてデータの異なるサブセットを派生する必要がある場合に特に役立ちます。
代替セレクタライブラリ
Reselect は Redux で最も広く使用されているセレクタライブラリですが、同様の問題を解決したり、Reselect の機能を拡張したりする他の多くのライブラリがあります。
`proxy-memoize`
`proxy-memoize` は、独自の 구현アプローチを使用する比較的新しいメモ化セレクタライブラリです。ES2015 の `Proxy` オブジェクトに依存して、ネストされた値の読み取り試行を追跡し、後続の呼び出しでネストされた値のみを比較して、変更されたかどうかを確認します。 これは、場合によっては Reselect よりも優れた結果を提供できます。
todo 記述の配列を派生するセレクタが良い例です。
import { createSelector } from 'reselect'
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
残念ながら、`todo.completed` フラグの切り替えなど、`state.todos` 内の他の値が変更された場合、派生配列は再計算されます。派生配列の*内容*は同じですが、入力 `todos` 配列が変更されたため、新しい出力配列を計算する必要があり、それには新しい参照があります。
`proxy-memoize` を使用した同じセレクタは次のようになります。
import { memoize } from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
Reselect とは異なり、`proxy-memoize` は `todo.text` フィールドのみがアクセスされていることを検出し、`todo.text` フィールドのいずれかが変更された場合にのみ残りを再計算します。
また、組み込みの `size` オプションがあり、単一のセレクタインスタンスに必要なキャッシュサイズを設定できます。
Reselect とはいくつかのトレードオフと違いがあります。
- すべての値は単一のオブジェクト引数として渡されます。
- 環境が ES2015 `Proxy` オブジェクトをサポートしている必要があります(IE11 は不可)。
- Reselect がより明示的であるのに対し、より魔法のようです。
- `Proxy` ベースの追跡動作に関するいくつかのエッジケースがあります。
- より新しく、広く使用されていません。
とはいえ、**Reselect の実行可能な代替手段として `proxy-memoize` の使用を検討することを公式に推奨します**。
`re-reselect`
https://github.com/toomuchdesign/re-reselect は、「キーセレクタ」を定義できるようにすることで、Reselect のキャッシュ動作を改善します。 これは、Reselect セレクタの複数のインスタンスを内部的に管理するために使用され、複数のコンポーネントでの使用を簡素化できます。
import { createCachedSelector } from 're-reselect'
const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId)
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
)
`reselect-tools`
複数の Reselect セレクタが互いにどのように関連しているか、およびセレクタが再計算された原因を追跡するのが難しい場合があります。 https://github.com/skortchmark9/reselect-tools は、セレクタの依存関係を追跡する方法と、それらの関係を視覚化し、セレクタ値を確認するための独自の DevTools を提供します。
`redux-views`
https://github.com/josepot/redux-views は `re-reselect` と似ており、一貫したキャッシュのために各アイテムに一意のキーを選択する方法を提供します。 Reselect のほぼドロップイン置換として設計され、実際には Reselect バージョン 5 の潜在的なオプションとして提案されました。
Reselect v5 提案
Reselect リポジトリでロードマップの議論を開始し、API を改善してより大きなキャッシュサイズをサポートしたり、コードベースを TypeScript で書き直したりするなど、Reselect の将来のバージョンに対する潜在的な拡張機能を検討しました。その他の改善が可能です。その議論に追加のコミュニティフィードバックを歓迎します。
Reselect v5 ロードマップの議論:目標と API 設計
React-Redux でのセレクタの使用
パラメータを使用したセレクタの呼び出し
セレクタ関数に追加の引数を渡したいことがよくあります。ただし、`useSelector` は常に、提供されたセレクタ関数を 1 つの引数(Redux ルート `state`)で呼び出します。
最も簡単な解決策は、匿名セレクタを `useSelector` に渡し、`state` と追加の引数の両方で実際のセレクタをすぐに呼び出すことです。
import { selectTodoById } from './todosSlice'
function TodoListitem({ todoId }) {
// Captures `todoId` from scope, gets `state` as an arg, and forwards both
// to the actual selector function to extract the result
const todo = useSelector(state => selectTodoById(state, todoId))
}
一意のセレクタインスタンスの作成
セレクタ関数を複数のコンポーネントで再利用する必要がある場合が多くあります。コンポーネントがすべて異なる引数でセレクタを呼び出す場合、メモ化が壊れます。セレクタは、同じ引数を連続して複数回認識することがないため、キャッシュされた値を返すことができません。
ここでの標準的なアプローチは、コンポーネントにメモ化されたセレクタの一意のインスタンスを作成し、それを `useSelector` で使用することです。これにより、各コンポーネントは独自のセレクタインスタンスに同じ引数を一貫して渡すことができ、そのセレクタは結果を正しくメモ化できます。
関数コンポーネントの場合、これは通常 `useMemo` または `useCallback` を使用して行われます。
import { makeSelectItemsByCategory } from './categoriesSlice'
function CategoryList({ category }) {
// Create a new memoized selector, for each component instance, on mount
const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])
const itemsByCategory = useSelector(state =>
selectItemsByCategory(state, category)
)
}
`connect` を使用するクラスコンポーネントの場合、これは `mapState` の高度な「ファクトリ関数」構文を使用して行うことができます。 `mapState` 関数が最初の呼び出しで新しい関数を返す場合、それは実際の `mapState` 関数として使用されます。これにより、新しいセレクタインスタンスを作成できるクロージャが提供されます。
import { makeSelectItemsByCategory } from './categoriesSlice'
const makeMapState = (state, ownProps) => {
// Closure - create a new unique selector instance here,
// and this will run once for every component instance
const selectItemsByCategory = makeSelectItemsByCategory()
const realMapState = (state, ownProps) => {
return {
itemsByCategory: selectItemsByCategory(state, ownProps.category)
}
}
// Returning a function here will tell `connect` to use it as
// `mapState` instead of the original one given to `connect`
return realMapState
}
export default connect(makeMapState)(CategoryList)
セレクタの効果的な使用
セレクタは Redux アプリケーションの一般的なパターンですが、誤用または誤解されることがよくあります。セレクタ関数を正しく使用するためのガイドラインを次に示します。
リデューサーと一緒にセレクタを定義する
セレクタ関数は、多くの場合、UI レイヤーで `useSelector` 呼び出しの内部に直接定義されます。ただし、これは、異なるファイルで定義されたセレクタ間に重複があり、関数が匿名であることを意味します。
他の関数と同様に、匿名関数をコンポーネントの外に抽出して名前を付けることができます。
const selectTodos = state => state.todos
function TodoList() {
const todos = useSelector(selectTodos)
}
しかし、アプリケーションの複数の部分が同じルックアップを使用したい場合があります。また、概念的には、todos
状態の構成方法に関する知識を todosSlice
ファイル内の実装の詳細として保持し、すべてを1か所にまとめたい場合があります。
このため、**対応するreducerと一緒に再利用可能なセレクターを定義することをお勧めします**。この場合、todosSlice
ファイルから selectTodos
をエクスポートできます。
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
export default todosSlice.reducer
// Export a reusable selector here
export const selectTodos = state => state.todos
こうすることで、todos スライス状態の構造を更新する場合、関連するセレクターがすぐそこにあり、アプリの他の部分を最小限に変更するだけで、同時に更新できます。
セレクターの使用のバランス
アプリケーションに*多すぎる*セレクターを追加することも可能です。**すべてのフィールドに個別のセレクター関数を追加することはお勧めできません!** これは、Reduxをすべてのフィールドにgetter/setter関数を持つJavaクラスのようなものに変えてしまいます。コードを*改善*するのではなく、おそらくコードを*悪化*させるでしょう - すべての追加セレクターを維持するには多くの追加作業が必要であり、どの値がどこで使用されているかを追跡することが難しくなります。
同様に、**すべてのセレクターをメモ化しないでください!** メモ化は、本当に結果を*派生*させており、*かつ*派生した結果が毎回新しい参照を作成する可能性がある場合にのみ必要です。**値を直接ルックアップして返すセレクター関数は、メモ化された関数ではなく、プレーンな関数である必要があります**。
メモ化するべき場合とすべきでない場合の例
// ❌ DO NOT memoize: will always return a consistent reference
const selectTodos = state => state.todos
const selectNestedValue = state => state.some.deeply.nested.field
const selectTodoById = (state, todoId) => state.todos[todoId]
// ❌ DO NOT memoize: deriving data, but will return a consistent result
const selectItemsTotal = state => {
return state.items.reduce((result, item) => {
return result + item.total
}, 0)
}
const selectAllCompleted = state => state.todos.every(todo => todo.completed)
// ✅ SHOULD memoize: returns new references when called
const selectTodoDescriptions = state => state.todos.map(todo => todo.text)
コンポーネントに必要な状態の再形成
セレクターは、直接ルックアップに限定される必要はありません - 内部で*必要な*変換ロジックを実行できます。これは、特定のコンポーネントに必要なデータを準備するのに特に役立ちます。
Redux状態は、状態を最小限に保つべきであるため、多くの場合「生の」形式でデータを持ち、多くのコンポーネントが同じデータを異なる方法で表示する必要がある場合があります。セレクターを使用して、状態を*抽出*するだけでなく、この特定のコンポーネントのニーズに合わせて*再形成*することもできます。これには、ルート状態の複数のスライスからのデータのプル、特定の値の抽出、データの異なる部分のマージ、または役立つその他の変換が含まれる場合があります。
コンポーネントにもこのロジックの一部が含まれていても問題ありませんが、再利用性とテスト容易性を高めるために、この変換ロジックをすべて個別のセレクターに抽出すると便利です。
必要に応じてセレクターをグローバル化する
スライスreducerとセレクターの記述には、本質的な不均衡があります。スライスreducerは、状態の1つの部分についてのみ認識しています - reducerにとって、そのstate
は、todoSlice
内のtodoの配列など、存在するすべてです。一方、セレクターは、*通常*Reduxルート状態全体を引数として取るように記述されます。これは、ルートreducerが(通常はアプリ全体のストア設定ロジックで)作成されるまで実際に定義されていないstate.todos
など、このスライスのデータがルート状態のどこに保持されているかを知る必要があることを意味します。
典型的なスライスファイルには、これらのパターンの両方が並んで含まれていることがよくあります。特に小規模または中規模のアプリでは、これで問題ありません。しかし、アプリのアーキテクチャによっては、スライスの状態がどこに保持されているかを*知らなくても*済むように、セレクターをさらに抽象化したい場合があります - それはセレクターに渡される必要があります。
このパターンを「セレクターのグローバル化」と呼びます。**「グローバル化された」セレクター**とは、Reduxルート状態を引数として受け取り、実際のロジックを実行するために関連する状態のスライスを見つける方法を知っているセレクターです。**「ローカライズされた」セレクター**とは、ルート状態のどこにあるかを知らなくても、状態の*一部だけ*を引数として期待するセレクターです。
// "Globalized" - accepts root state, knows to find data at `state.todos`
const selectAllTodosCompletedGlobalized = state =>
state.todos.every(todo => todo.completed)
// "Localized" - only accepts `todos` as argument, doesn't know where that came from
const selectAllTodosCompletedLocalized = todos =>
todos.every(todo => todo.completed)
「ローカライズされた」セレクターは、正しい状態のスライスを取得して渡す方法を知っている関数でラップすることで、「グローバル化された」セレクターに変換できます。
Redux ToolkitのcreateEntityAdapter
APIはこのパターンの例です。引数なしでtodosAdapter.getSelectors()
を呼び出すと、*エンティティスライス状態*を引数として期待する「ローカライズされた」セレクターのセットが返されます。todosAdapter.getSelectors(state => state.todos)
を呼び出すと、*Reduxルート状態*を引数として呼び出されることを期待する「グローバル化された」セレクターのセットが返されます。
セレクターの「ローカライズされた」バージョンを持つことにも、他の利点がある場合があります。たとえば、ルームを追跡するchatRoomsAdapter
や、各ルーム定義にメッセージを格納するためのchatMessagesAdapter
状態があるなど、ストアにネストされたcreateEntityAdapter
データの複数のコピーを保持する高度なシナリオがあるとします。各ルームのメッセージを直接検索することはできません - まずルームオブジェクトを取得し、そこからメッセージを選択する必要があります。これは、メッセージの「ローカライズされた」セレクターのセットがあれば簡単になります。
詳細情報
- セレクターライブラリ
- Reselect: https://github.com/reduxjs/reselect
proxy-memoize
: https://github.com/dai-shi/proxy-memoizere-reselect
: https://github.com/toomuchdesign/re-reselectreselect-tools
: https://github.com/skortchmark9/reselect-toolsredux-views
: https://github.com/josepot/redux-views
- Reselect v5 ロードマップの議論:目標と API 設計
- Randy Coulmanは、セレクターアーキテクチャとReduxセレクターをグローバル化するためのさまざまなアプローチに関する、トレードオフを含む優れたブログ記事シリーズを執筆しています。