Reactアプリケーションの状態管理のためのOSSライブラリである、React Reduxのバージョン7.1.0で追加された、Hooks APIの公式ドキュメントを翻訳していきます。
- React Redux Hooks公式ドキュメント翻訳(概要編)
- React Redux Hooks公式ドキュメント翻訳(useSelector編)
- React Redux Hooks公式ドキュメント翻訳(useDispatch, useStore編)
2021/1/11公開。原文リンクは以下。
・公式ドキュメント(React Redux Hooks):https://react-redux.js.org/api/hooks
#useSelector()
const result: any = useSelector(selector: Function, equalityFn?: Function)
useSelectorを使うことでReduxのstoreのstateにデータを登録することが可能になります。
Note: useSelectorは純粋関数である必要があります。なぜなら、複数回、任意のタイミングで実行される可能性があるからです。
useSelectorは概念的にはconnect関数に対して与えた引数mapToPropsに相当します。利点としては、Reduxのstore state全体をただの引数として扱える点です。前のコンポーネントのレンダリング後から、その参照先が普遍である限り、useSelctorはキャッシュしたselectorの値を返すので、関数コンポーネントのレンダリングをいつでも実行することができます。useSelectorはさらに、Redux storeを登録したり、actionがdispatchされるときにいつでもselectorを呼び出せます。
しかしながら、useSelectorとmapState関数のそれぞれで呼び出したselectorにはいくつか違いがあります。
・selectorはオブジェクトに限らず全ての値を返すことができます。selectorの返り値はuseSelectorの返り値として扱われます。
・actionがdispatchされたとき、useSelectorは前回の返り値と今の返り値を比較します。それらが異なる場合、コンポーネントは再レンダリングを強いられます。同じ場合は、そのコンポーネントは再レンダリングされません。
・useSelector関数は引数ownPropsを受け取りませんが、propsはクロージャを通す(以下の例を見てください)またはカリー化した関数を使うことで使用することができます。
・保存されたselctor(memorized selector)を扱う場合は特別な注意が必要です。(詳細は以下の例を見てください。)
・useSelectorはデフォルトでは等価比較に厳密な===を使います。曖昧比較は使いません。(次のセクションで詳細を説明します。)
Note: propsを扱う場合は、エラーを引き起こす特別なケースがいくつかあります。詳細は使用上の注意を参照してください。
1つの関数コンポーネントでuseSelectorは複数回呼び出されることがあります。useSelectorを呼び出す際には毎回、Reduxのstoreに登録されます。React Reduxバージョン7からuseSelectorは追加されたため、同一のコンポーネントでuseSelectorの複数回の呼び出しを引き起こすようなdispatchされたactionは、一回の再レンダリングのみで済みます。
#同一性の比較とアップデート
関数コンポーネントのレンダリングの際、引数に与えられたselector関数が呼ばれ、その結果がuseSelectorの返り値となります。(前回のコンポーネントのレンダリングと結果が同じ場合、キャッシュされた結果はselectorの再レンダリング無しで返されます。)
しかしながら、もし、selectorの結果が明らかに前の結果と異なる場合、Reduxのstoreにactionがdispatchされた時に、useSelectorは一回の再レンダリングのみを実施させます。Reduxバージョン7.1.0-alpha.5の時点では、デフォルトの比較は厳密比較(===)です。これは再レンダリングが必要かどうか決定する、mapState関数の結果を曖昧比較でチェックするconnect関数と異なる点です。これはuseSelectorを扱う上でいくつかの示唆を与えます。
mapStateでは全ての個々のフィールドは組み合わされたオブジェクトとして返されました。そこでは返されたオブジェクトが新しい参照をしたものかどうかは問題ではありませんでした。つまり、connect関数は単に個々のフィールドを比較しているだけです。useSelectorは、常に新しいオブジェクトを返すため、デフォルトでは毎回再レンダリングを強制します。もし、複数の値をstoreから取得したい場合は、以下のようにできます。
・useSelctorを複数回呼び出し、それぞれで単一のフィールドを返すようにする。
・1つのオブジェクトで保存されたselctor(memorized selector)を作るライブラリを使うようにする。しかし、1つの値が変更された場合は新しいオブジェクトを一つだけ返すようにする。
・同一性を比較する関数として、useSelctorの引数に、React ReduxのshallowEqual関数を使う。(以下例)
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
#useSelectorの使用例
基本的な使い方
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}
抜き出したいものを決めるために、クロージャを使ってpropsを扱う
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
###保存されたselector(memoizing selector)を使う
上記のようにインライン要素のselectorを扱う場合にuseSelectorを用いる際は、コンポーネントがレンダリングされる毎回で新しいselectorのインスタンスが作られます。これはselectorがなんのstateも持っていない場合の挙動です。しかしながら、例えばreselectライブラリのcreateSelector関数によって作られる、保存されたselector(memoizing selector)は内部にstateを持ちません。そして、それゆえに扱う際には注意が必要です。以下にmemoizing selectorの典型的な使用例を示します。
selectorがstateのみに依存しない場合、同じselectorのインスタンスがそれぞれのレンダリングで使われるようにするために、コンポーネントの外で宣言されることを確認してください。
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.isDone).length
)
export const DoneTodosCounter = () => {
const numOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{numOfDoneTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<DoneTodosCounter />
</>
)
}
selectorがコンポーネントのpropsに依存している場合も同様です。しかし、単一のコンポーネントで単一のインスタンスのみが使用可能なので注意してください。
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfTodosWithIsDoneValue = createSelector(
state => state.todos,
(_, isDone) => isDone,
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)
export const TodoCounterForIsDoneValue = ({ isDone }) => {
const numOfTodosWithIsDoneValue = useSelector(state =>
selectNumOfTodosWithIsDoneValue(state, isDone)
)
return <div>{numOfTodosWithIsDoneValue}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<TodoCounterForIsDoneValue isDone={true} />
</>
)
}
selectorが複数のコンポーネントのインスタンスで使われており、それらのコンポーネントのpropsに依存している場合、それぞれのコンポーネントのインスタンスがそれらのselectorのインスタンスを取得していることを確認してください。(なぜこれが必要なのかについてより詳細な説明は、ここを参照してください。)
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const makeNumOfTodosWithIsDoneSelector = () =>
createSelector(
state => state.todos,
(_, isDone) => isDone,
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)
export const TodoCounterForIsDoneValue = ({ isDone }) => {
const selectNumOfTodosWithIsDone = useMemo(
makeNumOfTodosWithIsDoneSelector,
[]
)
const numOfTodosWithIsDoneValue = useSelector(state =>
selectNumOfTodosWithIsDone(state, isDone)
)
return <div>{numOfTodosWithIsDoneValue}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<TodoCounterForIsDoneValue isDone={true} />
<span>Number of unfinished todos:</span>
<TodoCounterForIsDoneValue isDone={false} />
</>
)
}