Skip to main content

钩子

¥Hooks

React 的 "hooks" API 赋予函数组件使用本地组件状态、执行副作用等的能力。React 还允许我们编写 定制钩子,它允许我们提取可重用的钩子,以便在 React 的内置钩子之上添加我们自己的行为。

¥React's "hooks" APIs give function components the ability to use local component state, execute side effects, and more. React also lets us write custom hooks, which let us extract reusable hooks to add our own behavior on top of React's built-in hooks.

React Redux 包含自己的自定义钩子 API,允许你的 React 组件订阅 Redux 存储和调度操作。

¥React Redux includes its own custom hook APIs, which allow your React components to subscribe to the Redux store and dispatch actions.

提示

我们建议使用 React-Redux hooks API 作为 React 组件中的默认方法。

¥We recommend using the React-Redux hooks API as the default approach in your React components.

现有的 connect API 仍然有效并将继续受到支持,但 hooks API 更简单并且与 TypeScript 配合使用效果更好。

¥The existing connect API still works and will continue to be supported, but the hooks API is simpler and works better with TypeScript.

这些钩子首先在 v7.1.0 中添加。

¥These hooks were first added in v7.1.0.

在 React Redux 应用中使用 Hooks

¥Using Hooks in a React Redux App

connect() 一样,你应该首先将整个应用封装在 <Provider> 组件中,以使存储在整个组件树中可用:

¥As with connect(), you should start by wrapping your entire application in a <Provider> component to make the store available throughout the component tree:

const store = createStore(rootReducer)

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>,
)

从那里,你可以导入任何列出的 React Redux hooks API 并在你的函数组件中使用它们。

¥From there, you may import any of the listed React Redux hooks APIs and use them within your function components.

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 存储状态中提取数据以供此组件使用。

¥Allows you to extract data from the Redux store state for use in this component, using a selector function.

信息

选择器函数应该是 pure,因为它可能会在任意时间点执行多次。

¥The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.

有关编写和使用选择器函数的更多详细信息,请参阅 Redux 文档中的 使用 Redux:使用选择器导出数据

¥See Using Redux: Deriving Data with Selectors in the Redux docs for more details on writing and using selector functions.

将使用整个 Redux 存储状态作为其唯一参数来调用选择器。选择器可以返回任何值作为结果,包括直接返回嵌套在 state 内的值,或派生新值。选择器的返回值将用作 useSelector() 钩子的返回值。

¥The selector will be called with the entire Redux store state as its only argument. The selector may return any value as a result, including directly returning a value that was nested inside state, or deriving new values. The return value of the selector will be used as the return value of the useSelector() hook.

只要函数组件渲染,选择器就会运行(除非自上次渲染组件以来其引用没有更改,以便钩子可以返回缓存的结果,而无需重新运行选择器)。useSelector() 还将订阅 Redux 存储,并在分派操作时运行你的选择器。

¥The selector will be run whenever the function component renders (unless its reference hasn't changed since a previous render of the component so that a cached result can be returned by the hook without re-running the selector). useSelector() will also subscribe to the Redux store, and run your selector whenever an action is dispatched.

当一个 action 被调度时,useSelector() 会对之前的选择器结果值和当前结果值进行引用比较。如果它们不同,组件将被强制重新渲染。如果它们相同,则组件将不会重新渲染。useSelector() 默认使用严格的 === 引用相等性检查,而不是浅层相等性(有关更多详细信息,请参阅以下部分)。

¥When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render. useSelector() uses strict === reference equality checks by default, not shallow equality (see the following section for more details).

该选择器在概念上大致相当于 mapStateToPropsconnect 的参数

¥The selector is approximately equivalent to the mapStateToProps argument to connect conceptually.

你可以在单个函数组件内多次调用 useSelector()。每次调用 useSelector() 都会创建对 Redux 存储的单独订阅。由于 React Redux v7 中使用的 React 更新批处理行为,导致同一组件中的多个 useSelector() 返回新值的分派操作应该只会导致一次重新渲染。

¥You may call useSelector() multiple times within a single function component. Each call to useSelector() creates an individual subscription to the Redux store. Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple useSelector()s in the same component to return new values should only result in a single re-render.

信息

在选择器中使用 props 的潜在边缘情况可能会导致问题。有关更多详细信息,请参阅本页的 使用警告 部分。

¥There are potential edge cases with using props in selectors that may cause issues. See the Usage Warnings section of this page for further details.

平等比较和更新

¥Equality Comparisons and Updates

当函数组件渲染时,将调用提供的选择器函数,并从 useSelector() 钩子返回其结果。(如果缓存结果与组件先前渲染上的函数引用相同,则钩子可能会返回缓存结果,而无需重新运行选择器。)

¥When the function component renders, the provided selector function will be called and its result will be returned from the useSelector() hook. (A cached result may be returned by the hook without re-running the selector if it's the same function reference as on a previous render of the component.)

但是,当将操作分派到 Redux 存储时,如果选择器结果看起来与最后的结果不同,则 useSelector() 仅强制重新渲染。默认比较是严格的 === 参考比较。这与 connect() 不同,connect()mapState 调用的结果使用浅层相等检查来确定是否需要重新渲染。这对于你应该如何使用 useSelector() 有几个影响。

¥However, when an action is dispatched to the Redux store, useSelector() only forces a re-render if the selector result appears to be different than the last result. The default comparison is a strict === reference comparison. This is different than connect(), which uses shallow equality checks on the results of mapState calls to determine if re-rendering is needed. This has several implications on how you should use useSelector().

对于 mapState,所有单独的字段都以组合对象的形式返回。返回对象是否是新引用并不重要 - connect() 只是比较了各个字段。对于 useSelector(),默认情况下每次返回一个新对象都会强制重新渲染。如果你想从存储中检索多个值,你可以:

¥With mapState, all individual fields were returned in a combined object. It didn't matter if the return object was a new reference or not - connect() just compared the individual fields. With useSelector(), returning a new object every time will always force a re-render by default. If you want to retrieve multiple values from the store, you can:

  • 多次调用 useSelector(),每次调用返回一个字段值

    ¥Call useSelector() multiple times, with each call returning a single field value

  • 使用 Reselect 或类似的库创建一个记忆选择器,该选择器在一个对象中返回多个值,但仅当其中一个值发生更改时才返回一个新对象。

    ¥Use Reselect or a similar library to create a memoized selector that returns multiple values in one object, but only returns a new object when one of the values has changed.

  • 使用 React-Redux 中的 shallowEqual 函数作为 useSelector()equalityFn 参数,例如:

    ¥Use the shallowEqual function from React-Redux as the equalityFn argument to useSelector(), like:

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 参数,例如:

    ¥Use a custom equality function as the equalityFn argument to useSelector(), like:

import { useSelector } from 'react-redux'

// equality function
const customEqual = (oldValue, newValue) => oldValue === newValue

// later
const selectedData = useSelector(selectorReturningObject, customEqual)

可选的比较功能还可以使用 Lodash 的 _.isEqual() 或 Immutable.js 的比较功能。

¥The optional comparison function also enables using something like Lodash's _.isEqual() or Immutable.js's comparison capabilities.

useSelector 示例

¥useSelector Examples

基本用法:

¥Basic usage:

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
const counter = useSelector((state) => state.counter)
return <div>{counter}</div>
}

通过闭包使用 props 来确定要提取的内容:

¥Using props via closure to determine what to extract:

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>
}

使用记忆选择器

¥Using memoizing selectors

当将 useSelector 与内联选择器一起使用时(如上所示),每当渲染组件时都会创建选择器的新实例。只要选择器不维护任何状态,这种方法就有效。然而,记忆选择器(例如通过 createSelectorreselect 创建)确实具有内部状态,因此在使用它们时必须小心。你可以在下面找到记忆选择器的典型使用场景。

¥When using useSelector with an inline selector as shown above, a new instance of the selector is created whenever the component is rendered. This works as long as the selector does not maintain any state. However, memoizing selectors (e.g. created via createSelector from reselect) do have internal state, and therefore care must be taken when using them. Below you can find typical usage scenarios for memoizing selectors.

当选择器仅依赖于状态时,只需确保它在组件外部声明,以便每个渲染使用相同的选择器实例:

¥When the selector does only depend on the state, simply ensure that it is declared outside of the component so that the same selector instance is used for each render:

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,情况也是如此,但只会在单个组件的单个实例中使用:

¥The same is true if the selector depends on the component's props, but will only ever be used in a single instance of a single component:

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 时,你需要确保选择器的记忆行为正确配置(详细信息请参阅 此处)。

¥However, when the selector is used in multiple component instances and depends on the component's props, you need to ensure that selector's memoization behavior is properly configured (see here for details).

开发模式检查

¥Development mode checks

useSelector 在开发模式下运行一些额外的检查以监视意外行为。这些检查不会在生产版本中运行。

¥useSelector runs some extra checks in development mode to watch for unexpected behavior. These checks do not run in production builds.

信息

这些检查首先在 v8.1.0 中添加

¥These checks were first added in v8.1.0

选择器结果稳定性

¥Selector result stability

在开发中,所提供的选择器函数在第一次调用 useSelector 期间使用相同的参数运行额外的时间,并且如果选择器返回不同的结果(基于提供的 equalityFn),则会在控制台中触发警告。

¥In development, the provided selector function is run an extra time with the same parameter during the first call to useSelector, and warns in the console if the selector returns a different result (based on the equalityFn provided).

这很重要,因为当使用相同的输入再次调用时返回不同结果引用的选择器将导致不必要的重新渲染。

¥This is important, as a selector that returns a different result reference when called again with the same inputs will cause unnecessary rerenders.

// 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,
}))

如果选择器结果相当稳定(或者选择器已记忆),则它不会返回不同的结果,也不会记录任何警告。

¥If a selector result is suitably stable (or the selector is memoized), it will not return a different result and no warning will be logged.

默认情况下,这只会在第一次调用选择器时发生。你可以在提供程序中或在每次 useSelector 调用时配置检查。

¥By default, this will only happen when the selector is first called. You can configure the check in the Provider or at each useSelector call.

Global setting via context
<Provider store={store} stabilityCheck="always">
{children}
</Provider>
Individual hook setting
function Component() {
const count = useSelector(selectCount, {
devModeChecks: { stabilityCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, {
devModeChecks: { stabilityCheck: 'once' },
})
// ...
}

身份功能 (state => state) 检查

¥Identity Function (state => state) Check

突破性的改变!

这以前称为 noopCheck

¥This was previously referred to as noopCheck.

在开发中,会对选择器返回的结果进行检查。如果结果与传入的参数(即根状态)相同,它会在控制台中触发警告。

¥In development, a check is conducted on the result returned by the selector. It warns in the console if the result is the same as the parameter passed in, i.e. the root state.

返回整个根状态的 useSelector 调用几乎总是一个错误,因为这意味着只要状态发生任何变化,组件就会重新渲染。选择器应该尽可能细化,例如 state => state.some.nested.field

¥A useSelector call returning the entire root state is almost always a mistake, as it means the component will rerender whenever anything in state changes. Selectors should be as granular as possible, like 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)

默认情况下,这只会在第一次调用选择器时发生。你可以在提供程序中或在每次 useSelector 调用时配置检查。

¥By default, this will only happen when the selector is first called. You can configure the check in the Provider or at each useSelector call.

Global setting via context
<Provider store={store} identityFunctionCheck="always">
{children}
</Provider>
Individual hook setting
function Component() {
const count = useSelector(selectCount, {
devModeChecks: { identityFunctionCheck: 'never' },
})
// run once (default)
const user = useSelector(selectUser, {
devModeChecks: { identityFunctionCheck: 'once' },
})
// ...
}

connect 的比较

¥Comparisons with connect

传递给 useSelector()mapState 函数的选择器之间存在一些差异:

¥There are some differences between the selectors passed to useSelector() and a mapState function:

  • 选择器可以返回任何值作为结果,而不仅仅是一个对象。

    ¥The selector may return any value as a result, not just an object.

  • 选择器通常应该只返回一个值,而不是一个对象。如果你确实返回对象或数组,请务必使用记忆选择器以避免不必要的重新渲染。

    ¥The selector normally should return just a single value, and not an object. If you do return an object or an array, be sure to use a memoized selector to avoid unnecessary re-renders.

  • 选择器函数不接收 ownProps 参数。但是,可以通过闭包(请参阅上面的示例)或使用柯里化选择器来使用 props。

    ¥The selector function does not receive an ownProps argument. However, props can be used through closure (see the examples above) or by using a curried selector.

  • 你可以使用 equalityFn 选项自定义比较行为

    ¥You can use the equalityFn option to customize the comparison behavior

useDispatch()

import type { Dispatch } from 'redux'
const dispatch: Dispatch = useDispatch()

该钩子从 Redux 存储返回对 dispatch 函数的引用。你可以根据需要使用它来调度操作。

¥This hook returns a reference to the dispatch function from the Redux store. You may use it to dispatch actions as needed.

示例

¥Examples

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() 或类似的方式优化渲染行为,则可以避免由于更改的回调引用而导致不必要的子组件渲染。

¥When passing a callback using dispatch to a child component, you may sometimes want to memoize it with useCallback. If the child component is trying to optimize render behavior using React.memo() or similar, this avoids unnecessary rendering of child components due to the changed callback reference.

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 函数引用就会保持稳定。通常,该存储实例在应用中永远不会更改。

¥The dispatch function reference will be stable as long as the same store instance is being passed to the <Provider>. Normally, that store instance never changes in an application.

然而,React hooks lint 规则不知道 dispatch 应该是稳定的,并且会警告 dispatch 变量应该添加到 useEffectuseCallback 的依赖数组中。最简单的解决方案就是这样做:

¥However, the React hooks lint rules do not know that dispatch should be stable, and will warn that the dispatch variable should be added to dependency arrays for useEffect and useCallback. The simplest solution is to do just that:

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 存储的引用。

¥This hook returns a reference to the same Redux store that was passed in to the <Provider> component.

这个钩子可能不应该经常使用。优选 useSelector() 作为你的首选。然而,这对于确实需要访问存储的不太常见的场景可能很有用,例如更换 reducer。

¥This hook should probably not be used frequently. Prefer useSelector() as your primary choice. However, this may be useful for less common scenarios that do require access to the store, such as replacing reducers.

示例

¥Examples

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>
}

自定义上下文

¥Custom context

<Provider> 组件允许你通过 context 属性指定备用上下文。如果你正在构建一个复杂的可重用组件,并且你不希望你的存储与你的消费者应用可能使用的任何 Redux 存储发生冲突,那么这非常有用。

¥The <Provider> component allows you to specify an alternate context via the context prop. This is useful if you're building a complex reusable component, and you don't want your store to collide with any Redux store your consumers' applications might use.

要通过钩子 API 访问备用上下文,请使用钩子创建器函数:

¥To access an alternate context via the hooks API, use the hook creator functions:

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>
)
}

使用警告

¥Usage Warnings

旧的属性和 "僵尸子级"

¥Stale Props and "Zombie Children"

信息

自从我们在 v7.1.0 中发布以来,React-Redux hooks API 就已经可以投入生产了,我们建议使用 hooks API 作为组件中的默认方法。但是,可能会出现一些边缘情况,我们正在记录这些情况,以便你能够了解它们。

¥The React-Redux hooks API has been production-ready since we released it in v7.1.0, and we recommend using the hooks API as the default approach in your components. However, there are a couple of edge cases that can occur, and we're documenting those so that you can be aware of them.

在实践中,这些都是罕见的问题 - 我们收到的有关文档中这些问题的评论远远多于这些是应用中真正问题的实际报告。

¥In practice, these are a rare concern - we've received far more comments about these being in the docs than actual reports of these being a real problem in an app.

React Redux 实现中最困难的方面之一是确保如果你的 mapStateToProps 函数定义为 (state, ownProps),则每次都会使用 "latest" 属性调用它。到版本 4 为止,重复出现的错误报告涉及边缘情况,例如 mapState 函数针对数据刚刚被删除的列表项抛出错误。

¥One of the most difficult aspects of React Redux's implementation is ensuring that if your mapStateToProps function is defined as (state, ownProps), it will be called with the "latest" props every time. Up through version 4, there were recurring bugs reported involving edge case situations, such as errors thrown from a mapState function for a list item whose data had just been deleted.

从版本 5 开始,React Redux 试图保证与 ownProps 的一致性。在版本 7 中,这是使用 connect() 内部的自定义 Subscription 类实现的,该类形成嵌套层次结构。这确保了树中较低的连接组件仅在最近的连接祖级更新后才会收到存储更新通知。然而,这依赖于每个 connect() 实例覆盖内部 React 上下文的一部分,提供自己唯一的 Subscription 实例来形成嵌套,并使用新的上下文值渲染 <ReactReduxContext.Provider>

¥Starting with version 5, React Redux has attempted to guarantee that consistency with ownProps. In version 7, that is implemented using a custom Subscription class internally in connect(), which forms a nested hierarchy. This ensures that connected components lower in the tree will only receive store update notifications once the nearest connected ancestor has been updated. However, this relies on each connect() instance overriding part of the internal React context, supplying its own unique Subscription instance to form that nesting, and rendering the <ReactReduxContext.Provider> with that new context value.

使用钩子,无法渲染上下文提供程序,这意味着也没有嵌套的订阅层次结构。因此,"旧的属性" 和 "僵尸小孩" 问题可能会在依赖使用钩子而不是 connect() 的应用中再次出现。

¥With hooks, there is no way to render a context provider, which means there's also no nested hierarchy of subscriptions. Because of this, the "stale props" and "zombie child" issues may potentially re-occur in an app that relies on using hooks instead of connect().

具体来说,"旧的属性" 是指以下任何情况:

¥Specifically, "stale props" means any case where:

  • 选择器函数依赖于该组件的 props 来提取数据

    ¥a selector function relies on this component's props to extract data

  • 父组件将重新渲染并传递新的 props 作为操作的结果

    ¥a parent component would re-render and pass down new props as a result of an action

  • 但该组件的选择器函数在该组件有机会使用这些新属性重新渲染之前执行

    ¥but this component's selector function executes before this component has had a chance to re-render with those new props

根据使用的 props 以及当前的存储状态,这可能会导致选择器返回不正确的数据,甚至抛出错误。

¥Depending on what props were used and what the current store state is, this may result in incorrect data being returned from the selector, or even an error being thrown.

"僵尸子级" 特指以下情况:

¥"Zombie child" refers specifically to the case where:

  • 多个嵌套连接的组件在第一遍中安装,导致子组件在其父组件之前订阅存储

    ¥Multiple nested connected components are mounted in a first pass, causing a child component to subscribe to the store before its parent

  • 调度一个操作,从存储中删除数据,例如待办事项

    ¥An action is dispatched that deletes data from the store, such as a todo item

  • 结果,父组件将停止渲染该子组件

    ¥The parent component would stop rendering that child as a result

  • 但是,由于子级首先订阅,因此其订阅会在父级停止渲染之前运行。当它根据 props 从存储中读取值时,该数据不再存在,如果提取逻辑不小心,可能会导致抛出错误。

    ¥However, because the child subscribed first, its subscription runs before the parent stops rendering it. When it reads a value from the store based on props, that data no longer exists, and if the extraction logic is not careful, this may result in an error being thrown.

useSelector() 尝试通过捕获由于存储更新而执行选择器时(但不是在渲染期间执行选择器时)引发的所有错误来解决此问题。当发生错误时,组件将被强制渲染,此时选择器将再次执行。只要选择器是纯函数并且你不依赖于选择器抛出错误,这种方法就有效。

¥useSelector() tries to deal with this by catching all errors that are thrown when the selector is executed due to a store update (but not when it is executed during rendering). When an error occurs, the component will be forced to render, at which point the selector is executed again. This works as long as the selector is a pure function and you do not depend on the selector throwing errors.

如果你更愿意自己处理这个问题,这里有一些可能的选项,可以使用 useSelector() 完全避免这些问题:

¥If you prefer to deal with this issue yourself, here are some possible options for avoiding these problems altogether with useSelector():

  • 不要依赖选择器函数中的 props 来提取数据

    ¥Don't rely on props in your selector function for extracting data

  • 如果你确实依赖选择器函数中的 props 并且这些 props 可能会随着时间的推移而改变,或者你提取的数据可能基于可以删除的项目,请尝试防御性地编写选择器函数。不要直接进入 state.todos[props.id].name - 首先读取 state.todos[props.id],并在尝试读取 todo.name 之前验证它是否存在。

    ¥In cases where you do rely on props in your selector function and those props may change over time, or the data you're extracting may be based on items that can be deleted, try writing the selector functions defensively. Don't just reach straight into state.todos[props.id].name - read state.todos[props.id] first, and verify that it exists before trying to read todo.name.

  • 因为 connect 将必要的 Subscription 添加到上下文提供程序,并延迟评估子订阅,直到连接的组件重新渲染,所以只要连接的组件重新渲染,将连接的组件放在组件树中使用 useSelector 的组件正上方就可以防止这些问题。 - 由于与钩子组件相同的存储更新而渲染。

    ¥Because connect adds the necessary Subscription to the context provider and delays evaluating child subscriptions until the connected component has re-rendered, putting a connected component in the component tree just above the component using useSelector will prevent these issues as long as the connected component gets re-rendered due to the same store update as the hooks component.

表现

¥Performance

如前所述,默认情况下 useSelector() 会在分派操作后运行选择器函数时对所选值进行引用相等比较,并且仅在所选值发生更改时才会导致组件重新渲染。然而,与 connect() 不同的是,useSelector() 不会因为其父级重新渲染而阻止组件重新渲染,即使组件的 props 没有改变。

¥As mentioned earlier, by default useSelector() will do a reference equality comparison of the selected value when running the selector function after an action is dispatched, and will only cause the component to re-render if the selected value changed. However, unlike connect(), useSelector() does not prevent the component from re-rendering due to its parent re-rendering, even if the component's props did not change.

如果需要进一步的性能优化,你可以考虑将函数组件封装在 React.memo() 中:

¥If further performance optimizations are necessary, you may consider wrapping your function component in React.memo():

const CounterComponent = ({ name }) => {
const counter = useSelector((state) => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}

export const MemoizedCounterComponent = React.memo(CounterComponent)

钩子秘诀

¥Hooks Recipes

我们从最初的 alpha 版本中削减了 hooks API,专注于一组更简单的 API 原语。但是,你可能仍然希望在你自己的应用中使用我们尝试过的一些方法。这些示例应该可以复制并粘贴到你自己的代码库中。

¥We've pared down our hooks API from the original alpha release, focusing on a more minimal set of API primitives. However, you may still wish to use some of the approaches we tried in your own apps. These examples should be ready to copy and paste into your own codebase.

秘诀:useActions()

¥Recipe: useActions()

该钩子位于我们最初的 alpha 版本中,但在 v7.1.0-alpha.4 中基于 丹·阿布拉莫夫的建议 被删除。该建议是基于 "绑定动作创建者" 在基于钩子的用例中没有那么有用,并且导致过多的概念开销和语法复杂性。

¥This hook was in our original alpha release, but removed in v7.1.0-alpha.4, based on Dan Abramov's suggestion. That suggestion was based on "binding action creators" not being as useful in a hooks-based use case, and causing too much conceptual overhead and syntactic complexity.

你可能更愿意在组件中调用 useDispatch 钩子来检索对 dispatch 的引用,并根据需要在回调和效果中手动调用 dispatch(someActionCreator())。你还可以在自己的代码中使用 Redux bindActionCreators 函数来绑定动作创建者,或者像 const boundAddTodo = (text) => dispatch(addTodo(text)) 一样使用 "manually" 来绑定它们。

¥You should probably prefer to call the useDispatch hook in your components to retrieve a reference to dispatch, and manually call dispatch(someActionCreator()) in callbacks and effects as needed. You may also use the Redux bindActionCreators function in your own code to bind action creators, or "manually" bind them like const boundAddTodo = (text) => dispatch(addTodo(text)).

但是,如果你仍然想自己使用这个钩子,这里有一个可复制粘贴的版本,它支持将动作创建者作为单个函数、数组或对象传递。

¥However, if you'd like to still use this hook yourself, here's a copy-pastable version that supports passing in action creators as a single function, an array, or an object.

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()

¥Recipe: useShallowEqualSelector()

import { useSelector, shallowEqual } from 'react-redux'

export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}

使用钩子时的其他注意事项

¥Additional considerations when using hooks

在决定是否使用钩子时,需要考虑一些架构权衡。Mark Erikson 在他的两篇博客文章 关于 React Hooks、Redux 和关注点分离的思考Hook、HOC 和权衡 中很好地总结了这些内容。

¥There are some architectural trade offs to take into consideration when deciding whether to use hooks or not. Mark Erikson summarizes these nicely in his two blog posts Thoughts on React Hooks, Redux, and Separation of Concerns and Hooks, HOCs, and Tradeoffs.