メインコンテンツへスキップ

セレクターによるデータ導出

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

学習内容
  • 優れた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という単語と選択される値の説明を組み合わせたプレフィックスを付けることを推奨します。典型的な例は**selectTodoByIdselectFilteredTodosselectVisibleTodos**のようになります。

React-ReduxのuseSelectorフックを使ったことがあれば、セレクター関数の基本的な考え方にはお馴染みかもしれません - useSelectorに渡す関数はセレクターでなければなりません:

function TodoList() {
// This anonymous arrow function is a selector!
const todos = useSelector(state => state.todos)
}

セレクター関数は通常、Reduxアプリケーションの2つの異なる場所で定義されます:

  • スライスファイル内(リデューサーロジックと共に)

  • コンポーネントファイル内(コンポーネント外かuseSelector呼び出し内)

セレクター関数は、Reduxのルートステート全体にアクセスできる場所ならどこでも使用できます。これにはuseSelectorフック、connectmapState関数、ミドルウェア、サンク、サガなどが含まれます。例えば、サンクやミドルウェアはgetState引数にアクセスできるため、そこでセレクターを呼び出せます:

function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)

if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}

通常、セレクターをリデューサー内で使用することはできません。スライスリデューサーは自身のスライスにしかアクセスできませんが、ほとんどのセレクターはReduxルートステート全体を引数として受け取ることを期待しているためです。

セレクターによるステート構造のカプセル化

セレクター関数を使用する最初の理由は、Reduxステート構造を扱う際のカプセル化と再利用性のためです。

あるuseSelectorフックがReduxステートの特定部分を詳細に参照しているとします:

const data = useSelector(state => state.some.deeply.nested.field)

このコードは有効で正常に動作しますが、アーキテクチャ的には最適とは言えません。このフィールドにアクセスする必要がある複数のコンポーネントがある場合を想像してください。そのステートの場所を変更する必要が生じたらどうなるでしょうか?その値を参照するすべてのuseSelectorフックを変更しなければなりません。アクション作成をカプセル化するためにアクションクリエイターを使用するのと同様、特定のステートの場所に関する知識をカプセル化するために再利用可能なセレクターを定義することを推奨します。こうすれば、アプリケーションがそのデータを取得する必要がある場所ならどこでも、同じセレクター関数をコードベース内で複数回使用できます。

理想的には、リデューサー関数とセレクターだけが正確なステート構造を知っているべきです。そのため、ステートの場所を変更する場合でも、これら2つのロジックだけを更新すれば済みます

このため、コンポーネント内でセレクターを定義するよりも、スライスファイル内で直接再利用可能なセレクターを定義することが推奨されます。

セレクターについてよくある説明は、「ステートに対するクエリ」のようなものだというものです。クエリがどのようにデータを取得したかは気にせず、必要なデータを要求し結果を受け取ることだけを考えます。

メモ化によるセレクターの最適化

セレクター関数はしばしば比較的「高コスト」な計算を実行したり、新しいオブジェクトや配列参照となる派生値を作成する必要があります。これはアプリケーションパフォーマンスにとって懸念材料となる可能性があります:

  • useSelectormapStateで使用されるセレクターは、Reduxルートステートのどの部分が実際に更新されたかに関係なく、ディスパッチされたアクションのたびに再実行されます。入力ステートが変更されていない場合に高コストな計算を再実行することはCPU時間の無駄であり、ほとんどの場合入力は変更されていない可能性が高いです。

  • useSelectormapStateは、コンポーネントの再レンダーが必要かどうかを判断するために、戻り値の===参照等価性チェックに依存しています。セレクターが常に新しい参照を返す場合、派生データが実質的に前回と同じであっても、コンポーネントが再レンダーされてしまいます。これは特に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.filter(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が実際に変更されない限り不要な作業が実行されます。

このため、同じ入力が渡された場合に結果の再計算を回避できる最適化されたセレクターを記述する方法が必要です。ここでメモ化の概念が重要になります。

メモ化(Memoization)はキャッシュの一種です。関数への入力値を追跡し、入力値と結果を後で参照できるように保存します。関数が以前と同じ入力値で呼び出された場合、実際の処理をスキップして前回と同じ結果を返すことができます。これにより、入力値が変更されたときだけ処理が実行され、入力値が同じ場合は常に同じ結果参照が返されるため、パフォーマンスが最適化されます。

次に、メモ化されたセレクターを記述するためのオプションをいくつか見ていきましょう。

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を使用して、これらの値の1つ以上を入力として受け取り、新しい派生値を生成するメモ化されたセレクターを作成します:

const selectTodos = state => state.todos.items
const selectCurrentUser = state => state.users.currentUser

const selectTodosForCurrentUser = createSelector(
[selectTodos, selectCurrentUser],
(todos, currentUser) => {
console.log('Output selector running')
return todos.filter(todo => todo.ownerId === currentUser.userId)
}
)

const todosForCurrentUser1 = selectTodosForCurrentUser(state)
// Log: "Output selector running"

const todosForCurrentUser2 = selectTodosForCurrentUser(state)
// No log output

console.log(todosForCurrentUser1 === todosForCurrentUser2)
// true

2回目にselectTodosForCurrentUserを呼び出した際、「出力セレクター」が実行されなかったことに注目してください。selectTodosselectCurrentUserの結果が最初の呼び出しと同じだったため、selectTodosForCurrentUserは最初の呼び出しのメモ化された結果を返すことができたのです。

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)を呼び出すと、selectOtherField42.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であり、これはセレクタインスタンスごとに固有です。これにより、単一のセレクタ関数を異なる入力で複数箇所で再利用する際に問題が発生します。

解決策の一つは「セレクタファクトリ」を作成することです。これは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)
)

残念ながら、state.todos内の他の値(例: todo.completedフラグの切り替え)が変更されると、このセレクタは導出配列を再計算します。導出配列の内容は同一でも、入力todos配列が変更されたため新しい出力配列を計算する必要があり、新しい参照が生成されます。

proxy-memoizeを使用した同じセレクタは次のようになります:

import { memoize } from 'proxy-memoize'

const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)

Reselectとは異なり、proxy-memoizetodo.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 Roadmap Discussion: Goals and API Design

React-Reduxでのセレクター使用

パラメーター付きセレクター呼び出し

セレクター関数に追加引数を渡したい場合がありますが、useSelectorは常に単一引数(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ファイル内の実装詳細として一元管理したい場合があるでしょう。

このため、対応するreducerと一緒に再利用可能なセレクターを定義するのが良いプラクティスです。この例では、todosSliceファイルからselectTodosをエクスポートできます:

src/features/todos/todosSlice.js
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をJavaクラスのように各フィールドにgetter/setter関数を持たせるような状態にし、コード改善どころか可読性を低下させます - 大量の追加セレクターを維持管理するのは多大な労力がかかり、どの値がどこで使用されているか追跡しづらくなります。

同様に、すべてのセレクターをメモ化しないようにしてください! メモ化が必要なのは、セレクターが実行のたびに新しい参照を返す場合や、実行する計算ロジックが高コストな場合のみです。値を直接参照して返すセレクター関数は、メモ化せずプレーンな関数にすべきです

メモ化が必要な場合/不要な場合の例:

// ❌ 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]

// 🤔 MAYBE memoize: deriving data, but will return a consistent result.
// Memoization might be useful if the selector is used in many places
// or the list being iterated over is long.
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は自身の状態部分のみ認識します - reducerにとってstateは(todoSliceのtodos配列など)存在するすべてです。一方セレクターは通常、Reduxルート状態全体を引数として受け取るように記述されます。これはセレクターが、ルート状態内での自身のデータ位置(state.todosなど)を知っている必要があることを意味します。実際にはこの位置関係は(通常アプリ全体のストア設定ロジックで作成される)ルートreducerが定義するまで確定しません。

一般的なスライスファイルではこれらのパターンが並存します。これは特に中小規模アプリでは問題ありません。ただしアプリのアーキテクチャによっては、セレクターがスライス状態の位置を認識しないよう、さらに抽象化したい場合があるでしょう。

このパターンを「セレクターのグローバル化」と呼びます。グローバル化されたセレクターは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ルート状態を引数として受け取る「グローバライズされた」セレクターのセットが返されます。

「ローカライズされた」セレクターには他の利点もあります。例えば、createEntityAdapterデータの複数コピーをストア内でネストして保持する高度なシナリオを考えます。chatRoomsAdapterがルームを追跡し、各ルーム定義にはメッセージを保存するchatMessagesAdapter状態がある場合です。各ルームのメッセージを直接検索することはできません。まずルームオブジェクトを取得し、そこからメッセージを選択する必要があります。メッセージ用の「ローカライズされた」セレクターセットがあれば、この操作が容易になります。

参考情報