フック
Reactの"フック" APIにより、関数コンポーネントはローカルコンポーネントの状態を使用したり、副作用を実行したりすることができるようになります。Reactでは、カスタムフックを作成することもでき、これにより、Reactの組み込みフックの上に独自の動作を追加する再利用可能なフックを抽出できます。
React Reduxには独自のCustom Hook APIが含まれており、ReactコンポーネントがReduxストアを購読し、アクションをディスパッチすることを可能にします。
Reactコンポーネントでは、React-ReduxフックAPIをデフォルトのアプローチとして使用することをお勧めします。
既存の`connect` APIはまだ機能しており、引き続きサポートされますが、フックAPIの方がシンプルで、TypeScriptとの互換性も優れています。
これらのフックは、v7.1.0で初めて追加されました。
React Reduxアプリケーションでのフックの使用
`connect()`と同様に、ストア全体を`
const store = createStore(rootReducer)
// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>,
)
そこから、リストされているReact ReduxフックAPIをインポートし、関数コンポーネント内で使用できます。
useSelector()
type RootState = ReturnType<typeof store.getState>
type SelectorFn = <Selected>(state: RootState) => Selected
type EqualityFn = (a: any, b: any) => boolean
export type DevModeCheckFrequency = 'never' | 'once' | 'always'
interface UseSelectorOptions {
equalityFn?: EqualityFn
devModeChecks?: {
stabilityCheck?: DevModeCheckFrequency
identityFunctionCheck?: DevModeCheckFrequency
}
}
const result: Selected = useSelector(
selector: SelectorFn,
options?: EqualityFn | UseSelectorOptions
)
セレクター関数を使用して、Reduxストアの状態からこのコンポーネントで使用するためのデータを取得できます。
セレクター関数は、複数回、任意の時点で実行される可能性があるため、純粋関数である必要があります。
セレクター関数の記述と使用方法の詳細については、ReduxのドキュメントのReduxの使用:セレクターを使用したデータの導出を参照してください。
セレクターは、Reduxストアの状態全体を唯一の引数として受け取ります。セレクターは、`state`の中にネストされた値を直接返すことや、新しい値を導出することを含め、任意の値を返すことができます。セレクターの戻り値は、`useSelector()`フックの戻り値として使用されます。
セレクターは、関数コンポーネントがレンダリングされるたびに実行されます(前のコンポーネントのレンダリング以降に参照が変更されていない場合、セレクターを再実行せずにフックによってキャッシュされた結果を返すことができます)。`useSelector()`はReduxストアを購読し、アクションがディスパッチされるたびにセレクターを実行します。
アクションがディスパッチされると、`useSelector()`は、以前のセレクターの結果値と現在の結果値の参照比較を行います。それらが異なる場合、コンポーネントは再レンダリングを強制されます。それらが同じである場合、コンポーネントは再レンダリングされません。`useSelector()`はデフォルトで厳密な`===`参照等価チェックを使用し、浅い等価性チェックは使用しません(詳細は次のセクションを参照)。
セレクターは、概念的には`connect`の`mapStateToProps`引数とほぼ同等です。
単一の関数コンポーネント内で`useSelector()`を複数回呼び出すことができます。`useSelector()`を呼び出すたびに、Reduxストアへの個々のサブスクリプションが作成されます。React Redux v7で使用されているReact更新バッチングの動作により、同じコンポーネント内の複数の`useSelector()`が新しい値を返す原因となるディスパッチされたアクションは、*1回だけ*再レンダリングされるはずです。
セレクターでpropsを使用することで発生する可能性のある潜在的なエッジケースがあります。詳細は、このページの使用上の警告セクションを参照してください。
等価比較と更新
関数コンポーネントがレンダリングされると、指定されたセレクター関数が呼び出され、その結果が`useSelector()`フックから返されます。(コンポーネントの以前のレンダリングと同じ関数参照である場合、フックによってキャッシュされた結果がセレクターを再実行せずに返される場合があります。)
ただし、アクションがReduxストアにディスパッチされると、`useSelector()`は、セレクターの結果が最後の実行結果と異なる場合にのみ、再レンダリングを強制します。デフォルトの比較は厳密な`===`参照比較です。これは、`mapState`呼び出しの結果に対して浅い等価チェックを使用して再レンダリングが必要かどうかを判断する`connect()`とは異なります。これは、`useSelector()`を使用する方法にいくつかの影響を与えます。
`mapState`では、すべての個々のフィールドが結合されたオブジェクトで返されました。戻り値のオブジェクトが新しい参照かどうかは問題ではありませんでした。`connect()`は個々のフィールドを比較するだけでした。`useSelector()`では、毎回新しいオブジェクトを返すことは、デフォルトで*常に*再レンダリングを強制します。ストアから複数の値を取得する場合は、次のことができます。
- `useSelector()`を複数回呼び出し、各呼び出しで単一のフィールド値を返します。
- Reselectまたは同様のライブラリを使用して、複数の値を1つのオブジェクトで返すメモ化されたセレクターを作成しますが、値のいずれかが変更された場合にのみ新しいオブジェクトを返します。
- `useSelector()`の`equalityFn`引数として、React-Reduxの`shallowEqual`関数を使用します。
import { shallowEqual, useSelector } from 'react-redux'
// Pass it as the second argument directly
const selectedData = useSelector(selectorReturningObject, shallowEqual)
// or pass it as the `equalityFn` field in the options argument
const selectedData = useSelector(selectorReturningObject, {
equalityFn: shallowEqual,
})
- `useSelector()`の`equalityFn`引数として、カスタム等価関数を使用します。
import { useSelector } from 'react-redux'
// equality function
const customEqual = (oldValue, newValue) => oldValue === newValue
// later
const selectedData = useSelector(selectorReturningObject, customEqual)
オプションの比較関数を使用することで、Lodashの`_.isEqual()`やImmutable.jsの比較機能などを使用することもできます。
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>
}
メモ化されたセレクターの使用
上記のようにインラインセレクターで`useSelector`を使用する場合、コンポーネントがレンダリングされるたびにセレクターの新しいインスタンスが作成されます。セレクターが状態を保持しない限り、これは機能します。ただし、メモ化されたセレクター(例:`reselect`の`createSelector`を使用して作成されたセレクター)は内部状態を持つため、それらを使用する際には注意が必要です。以下に、メモ化されたセレクターの一般的な使用方法を示します。
セレクターが状態のみに依存する場合、コンポーネントの外側に宣言して、各レンダリングで同じセレクターインスタンスが使用されるようにします。
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((todo) => todo.completed).length,
)
export const CompletedTodosCounter = () => {
const numCompletedTodos = useSelector(selectNumCompletedTodos)
return <div>{numCompletedTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of completed todos:</span>
<CompletedTodosCounter />
</>
)
}
セレクターがコンポーネントのpropsに依存する場合も同様ですが、単一のコンポーネントの単一のインスタンスでのみ使用される場合に限ります。
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectCompletedTodosCount = createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) =>
todos.filter((todo) => todo.completed === completed).length,
)
export const CompletedTodosCount = ({ completed }) => {
const matchingCount = useSelector((state) =>
selectCompletedTodosCount(state, completed),
)
return <div>{matchingCount}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<CompletedTodosCount completed={true} />
</>
)
}
ただし、セレクターが複数のコンポーネントインスタンスで使用され、コンポーネントのpropsに依存する場合は、セレクターのメモ化動作が適切に構成されていることを確認する必要があります(詳細についてはこちらを参照)。
開発モードのチェック
useSelector
は開発モードで追加のチェックを実行して、予期しない動作を監視します。これらのチェックは本番ビルドでは実行されません。
これらのチェックは、v8.1.0で初めて追加されました。
セレクター結果の安定性
開発モードでは、提供されたセレクター関数は、`useSelector`への最初の呼び出し中に同じパラメーターを使用して追加で1回実行され、セレクターが異なる結果を返す場合(提供された`equalityFn`に基づく)、コンソールに警告が表示されます。
これは重要です。なぜなら、**同じ入力を与えて再度呼び出されたときに異なる結果参照を返すセレクターは、不要な再レンダリングを引き起こすため**です。
// this selector will return a new object reference whenever called,
// which causes the component to rerender after *every* action is dispatched
const { count, user } = useSelector((state) => ({
count: state.count,
user: state.user,
}))
セレクターの結果が適切に安定している場合(またはセレクターがメモ化されている場合)、異なる結果は返されず、警告はログに記録されません。
デフォルトでは、これはセレクターが最初に呼び出された場合にのみ発生します。Providerまたは各`useSelector`呼び出しでチェックを構成できます。
<Provider store={store} stabilityCheck="always">
{children}
</Provider>
function Component() {
const count = useSelector(selectCount, {
devModeChecks: { stabilityCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, {
devModeChecks: { stabilityCheck: 'once' },
})
// ...
}
恒等関数(`state => state`) チェック
これは以前は`noopCheck`と呼ばれていました。
開発モードでは、セレクターによって返された結果に対してチェックが行われます。結果が渡されたパラメーター(つまり、ルート状態)と同じである場合、コンソールに警告が表示されます。
**ルート状態全体を返す`useSelector`の呼び出しは、ほとんどの場合間違いです。**これは、状態の*何か*が変更されるたびにコンポーネントが再レンダリングされることを意味します。セレクターは、`state => state.some.nested.field`のように、できるだけ粒度を細かくする必要があります。
// BAD: this selector returns the entire state, meaning that the component will rerender unnecessarily
const { count, user } = useSelector((state) => state)
// GOOD: instead, select only the state you need, calling useSelector as many times as needed
const count = useSelector((state) => state.count.value)
const user = useSelector((state) => state.auth.currentUser)
デフォルトでは、これはセレクターが最初に呼び出された場合にのみ発生します。Providerまたは各`useSelector`呼び出しでチェックを構成できます。
<Provider store={store} identityFunctionCheck="always">
{children}
</Provider>
function Component() {
const count = useSelector(selectCount, {
devModeChecks: { identityFunctionCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, {
devModeChecks: { identityFunctionCheck: 'once' },
})
// ...
}
connect
との比較
`useSelector()`に渡されるセレクターと`mapState`関数にはいくつかの違いがあります。
- セレクターは、オブジェクトだけでなく、任意の値を返すことができます。
- セレクターは通常、オブジェクトではなく、単一の値を返す*べき*です。オブジェクトまたは配列を返す場合は、不要な再レンダリングを回避するために、メモ化されたセレクターを使用してください。
- セレクター関数は`ownProps`引数を受け取りません。ただし、クロージャ(上記の例を参照)またはカリー化されたセレクターを使用して、propsを使用できます。
- `equalityFn`オプションを使用して、比較動作をカスタマイズできます。
useDispatch()
import type { Dispatch } from 'redux'
const dispatch: Dispatch = useDispatch()
このフックは、Reduxストアのdispatch
関数の参照を返します。必要に応じてアクションをディスパッチするために使用できます。
例
import React from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}
dispatch
を使用してコールバックを子コンポーネントに渡す場合、useCallback
でメモ化したい場合があります。子コンポーネントがReact.memo()
などでレンダリング動作の最適化を試みている場合、これにより、変更されたコールバック参照によって子コンポーネントが不要にレンダリングされるのを防ぎます。
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch],
)
return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}
export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))
同じストアインスタンスが<Provider>
に渡されている限り、dispatch
関数の参照は安定します。通常、アプリケーションではそのストアインスタンスは変更されません。
しかし、Reactフックのlintルールは、dispatch
が安定していることを認識しておらず、useEffect
とuseCallback
の依存関係配列にdispatch
変数を追加する必要があるという警告が表示されます。最も簡単な解決策は、まさにそれを行うことです。
export const Todos = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchTodos())
// Safe to add dispatch to the dependencies array
}, [dispatch])
}
useStore()
import type { Store } from 'redux'
const store: Store = useStore()
このフックは、<Provider>
コンポーネントに渡されたのと同じReduxストアへの参照を返します。
このフックは頻繁に使用すべきではありません。主要な選択肢としてuseSelector()
を優先してください。ただし、リデューサの置き換えなど、ストアへのアクセスが必要なあまり一般的ではないシナリオで役立つ場合があります。
例
import React from 'react'
import { useStore } from 'react-redux'
export const ExampleComponent = ({ value }) => {
const store = useStore()
const onClick = () => {
// Not _recommended_, but safe
// This avoids subscribing to the state via `useSelector`
// Prefer moving this logic into a thunk instead
const numTodos = store.getState().todos.length
}
// EXAMPLE ONLY! Do not do this in a real app.
// The component will not automatically update if the store state changes
return <div>{store.getState().todos.length}</div>
}
カスタムコンテキスト
<Provider>
コンポーネントでは、context
プロパティを使用して代替コンテキストを指定できます。これは、複雑で再利用可能なコンポーネントを構築していて、ストアが利用者のアプリケーションで使用されるReduxストアと衝突しないようにしたい場合に役立ちます。
フックAPIを介して代替コンテキストにアクセスするには、フック作成関数を使用します。
import React from 'react'
import {
Provider,
createStoreHook,
createDispatchHook,
createSelectorHook,
} from 'react-redux'
const MyContext = React.createContext(null)
// Export your custom hooks if you wish to use them in other files.
export const useStore = createStoreHook(MyContext)
export const useDispatch = createDispatchHook(MyContext)
export const useSelector = createSelectorHook(MyContext)
const myStore = createStore(rootReducer)
export function MyProvider({ children }) {
return (
<Provider context={MyContext} store={myStore}>
{children}
</Provider>
)
}
使用上の注意
古いプロパティと「ゾンビチャイルド」
React-ReduxフックAPIは、v7.1.0でリリースされて以来、本番環境で使用できる状態になっており、コンポーネントでのデフォルトのアプローチとしてフックAPIを使用することをお勧めします。ただし、発生する可能性のあるいくつかのエッジケースがあり、それらについて認識できるように文書化しています。
実際には、これらはまれな懸念事項です。これらがドキュメントにあることについてのコメントよりも、アプリケーションでの実際の問題に関する報告の方がはるかに少ないです。
React Reduxの実装において最も難しい側面の1つは、mapStateToProps
関数が(state, ownProps)
として定義されている場合、常に「最新」のプロパティを使用して呼び出されることを保証することです。バージョン4までは、データが削除されたばかりのリストアイテムのmapState
関数からスローされるエラーなど、エッジケースの状況に関連する再発するバグが報告されていました。
バージョン5以降、React ReduxはownProps
との整合性を保証しようと試みてきました。バージョン7では、内部的にconnect()
内のカスタムSubscription
クラスを使用して実装されています。これは、ツリーの下位にある接続されたコンポーネントが、最も近い接続された祖先が更新された後にのみストア更新通知を受け取ることを保証します。ただし、これは各connect()
インスタンスが内部Reactコンテキストの一部をオーバーライドし、独自のSubscription
インスタンスを提供してネストを形成し、その新しいコンテキスト値で<ReactReduxContext.Provider>
をレンダリングすることに依存しています。
フックを使用すると、コンテキストプロバイダーをレンダリングする方法がないため、サブスクリプションのネストされた階層もありません。このため、「古いプロパティ」と「ゾンビチャイルド」の問題が、connect()
の代わりにフックを使用するアプリケーションで発生する可能性があります。
具体的には、「古いプロパティ」とは、次のいずれかの場合を意味します。
- セレクター関数が、データの抽出にこのコンポーネントのプロパティに依存している
- 親コンポーネントがアクションの結果として再レンダリングし、新しいプロパティを渡す
- しかし、このコンポーネントのセレクター関数は、このコンポーネントがそれらの新しいプロパティで再レンダリングされる前に実行される
使用されたプロパティと現在のストアの状態によっては、セレクターから不正確なデータが返される、またはエラーがスローされる可能性があります。
「ゾンビチャイルド」は、次の場合を具体的に指します。
- 複数のネストされた接続済みコンポーネントが最初のパスでマウントされ、子コンポーネントが親よりも先にストアをサブスクライブする
- TODOアイテムなど、ストアからデータを削除するアクションがディスパッチされる
- 親コンポーネントはその子コンポーネントのレンダリングを停止する
- ただし、子が先にサブスクライブしたため、そのサブスクリプションは親がレンダリングを停止する前に実行されます。プロパティに基づいてストアから値を読み取ると、そのデータは存在しなくなり、抽出ロジックが慎重でない場合、エラーがスローされる可能性があります。
useSelector()
は、ストアの更新によってセレクターが実行されたときにスローされるすべてのエラー(レンダリング中に実行されたときではない)をキャッチすることで、これに対処しようとします。エラーが発生すると、コンポーネントは強制的にレンダリングされ、その時点でセレクターが再度実行されます。これは、セレクターが純粋関数であり、セレクターがエラーをスローすることに依存しない限り機能します。
自分でこの問題に対処したい場合は、useSelector()
でこれらの問題を完全に回避するためのいくつかのオプションがあります。
- セレクター関数でデータの抽出にプロパティに依存しない
- セレクター関数でプロパティに依存し、それらのプロパティが時間の経過とともに変化する可能性がある場合、または抽出するデータが削除できるアイテムに基づいている場合は、セレクター関数を防御的に記述してみてください。
state.todos[props.id].name
に直接アクセスするのではなく、最初にstate.todos[props.id]
を読み取り、todo.name
を読み取る前に存在することを確認します。 connect
は必要なSubscription
をコンテキストプロバイダーに追加し、接続されたコンポーネントが再レンダリングされるまで子サブスクリプションの評価を遅延させるため、useSelector
を使用するコンポーネントのすぐ上に接続されたコンポーネントをコンポーネントツリーに配置すると、フックコンポーネントと同じストア更新によって接続されたコンポーネントが再レンダリングされる限り、これらの問題は防止されます。
これらのシナリオの詳細については、以下を参照してください。
パフォーマンス
前述のように、デフォルトではuseSelector()
は、アクションがディスパッチされた後にセレクター関数を実行するときに、選択された値の参照の等価性を比較し、選択された値が変更された場合にのみコンポーネントを再レンダリングします。ただし、connect()
とは異なり、useSelector()
は、コンポーネントのプロパティが変更されなくても、親が再レンダリングすることによるコンポーネントの再レンダリングを防ぎません。
さらにパフォーマンスを最適化する必要がある場合は、関数コンポーネントをReact.memo()
でラップすることを検討できます。
const CounterComponent = ({ name }) => {
const counter = useSelector((state) => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}
export const MemoizedCounterComponent = React.memo(CounterComponent)
フックレシピ
元のアルファリリースからのフックAPIを削減し、より最小限のAPIプリミティブのセットに焦点を当てています。ただし、独自のアプリケーションで試したアプローチの一部を使用したい場合があります。これらの例は、独自のコードベースにコピーして貼り付ける準備ができています。
レシピ:useActions()
このフックは元のアルファリリースに含まれていましたが、Dan Abramovの提案に基づいてv7.1.0-alpha.4
で削除されました。その提案は、「アクションクリエーターのバインド」がフックベースのユースケースではそれほど役立たず、概念的なオーバーヘッドと構文の複雑さを引き起こすことに基づいていました。
コンポーネントでuseDispatch
フックを呼び出してdispatch
への参照を取得し、必要に応じてコールバックとエフェクトでdispatch(someActionCreator())
を手動で呼び出す方が良いでしょう。独自のコードでReduxのbindActionCreators
関数を使用してアクションクリエーターをバインドするか、const boundAddTodo = (text) => dispatch(addTodo(text))
のように手動でバインドすることもできます。
ただし、このフックを自分で使用したい場合は、アクションクリエーターを単一の関数、配列、またはオブジェクトとして渡すことをサポートする、コピーして貼り付けることができるバージョンを以下に示します。
import { bindActionCreators } from 'redux'
import { useDispatch } from 'react-redux'
import { useMemo } from 'react'
export function useActions(actions, deps) {
const dispatch = useDispatch()
return useMemo(
() => {
if (Array.isArray(actions)) {
return actions.map((a) => bindActionCreators(a, dispatch))
}
return bindActionCreators(actions, dispatch)
},
deps ? [dispatch, ...deps] : [dispatch],
)
}
レシピ:useShallowEqualSelector()
import { useSelector, shallowEqual } from 'react-redux'
export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}
フックを使用する場合の追加の考慮事項
フックを使用するかどうかを決定する際に考慮すべきアーキテクチャ上のトレードオフがいくつかあります。Mark Eriksonは、彼の2つのブログ投稿React Hooks、Redux、および懸念事項の分離に関する考察とHooks、HOC、およびトレードオフでこれらをうまくまとめています。