Hooks
React 的 "hooks" API 使函数组件能够使用本地组件状态、执行副作用等等。React 还允许我们编写 自定义 hooks,这让我们可以提取可重用的 hooks,在 React 的内置 hooks 之上添加我们自己的行为。
React Redux 包含它自己的自定义 hook API,允许您的 React 组件订阅 Redux store 并派发操作。
我们建议在 React 组件中使用 React-Redux hooks API 作为默认方法。
现有的 connect
API 仍然有效,并将继续得到支持,但 hooks API 更简单,并且与 TypeScript 配合得更好。
这些 hooks 最初是在 v7.1.0 中添加的。
在 React Redux 应用中使用 Hooks
与 connect()
一样,您应该首先将整个应用程序包装在 <Provider>
组件中,以使存储在整个组件树中可用。
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,并在您的函数组件中使用它们。
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()
hook 的返回值。
选择器将在函数组件渲染时运行(除非其引用自组件上一次渲染以来没有改变,以便 hook 可以返回缓存的结果而无需重新运行选择器)。useSelector()
还会订阅 Redux 存储,并在每次调度操作时运行您的选择器。
当分发一个 action 时,useSelector()
会对之前的选择器结果值和当前结果值进行引用比较。如果它们不同,组件将被强制重新渲染。如果它们相同,组件将不会重新渲染。useSelector()
默认使用严格的 ===
引用相等性检查,而不是浅层相等性(有关更多详细信息,请参阅下一节)。
选择器在概念上与 connect
的 mapStateToProps
参数 相当。
您可以在单个函数组件中多次调用 useSelector()
。每次调用 useSelector()
都会创建一个对 Redux 存储的单独订阅。由于 React Redux v7 中使用的 React 更新批处理行为,分发的 action 会导致同一组件中的多个 useSelector()
返回新值,应该只导致一次重新渲染。
在选择器中使用 props 可能存在潜在的边缘情况,可能会导致问题。有关更多详细信息,请参阅本页的 使用警告 部分。
相等性比较和更新
当函数组件渲染时,提供的选择器函数将被调用,其结果将从 useSelector()
钩子中返回。(如果钩子中的函数引用与组件上一次渲染时的函数引用相同,则可能会返回缓存的结果,而不会重新运行选择器。)
但是,当向 Redux 存储分发一个 action 时,useSelector()
只有在选择器结果看起来与上次结果不同时才会强制重新渲染。默认比较是严格的 ===
引用比较。这与 connect()
不同,connect()
对 mapState
调用的结果使用浅层相等性检查来确定是否需要重新渲染。这对您如何使用 useSelector()
有一些影响。
使用 mapState
时,所有单个字段都将返回到一个组合对象中。返回的对象是否是新的引用并不重要 - connect()
只比较单个字段。使用 useSelector()
时,每次都返回一个新对象将始终强制默认情况下重新渲染。如果您想从存储中检索多个值,您可以
- 多次调用
useSelector()
,每次调用返回一个单个字段值 - 使用 Reselect 或类似库创建记忆化选择器,该选择器在一个对象中返回多个值,但仅在其中一个值发生更改时才返回新对象。
- 使用 React-Redux 中的
shallowEqual
函数作为useSelector()
的equalityFn
参数,例如
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` 时会额外执行一次,并使用相同的参数。如果选择器返回的结果不同(基于提供的 `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
参数。但是,可以通过闭包(参见上面的示例)或使用柯里化选择器来使用道具。 - 您可以使用
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
应该保持稳定,并且会警告应该将dispatch
变量添加到useEffect
和useCallback
的依赖项数组中。最简单的解决方案就是这样做
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()
作为您的主要选择。但是,这可能对需要访问存储的不太常见的场景有用,例如替换 reducer。
示例
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 的 hooks API 自从我们在 v7.1.0 版本中发布以来,就已经可以投入生产环境了,并且**我们建议在您的组件中使用 hooks API 作为默认方法**。但是,可能会出现一些边缘情况,**我们正在记录这些情况,以便您了解它们**。
实际上,这些问题很少见 - 我们收到的关于这些问题在文档中的评论比实际报告这些问题在应用程序中成为真正问题的评论要多得多。
React Redux 实现中最困难的方面之一是确保如果您的 mapStateToProps
函数定义为 (state, ownProps)
,它将在每次调用时都使用“最新”的 props。在版本 4 之前,经常有关于边缘情况的错误报告,例如从列表项的 mapState
函数中抛出错误,而该列表项的数据刚刚被删除。
从版本 5 开始,React Redux 试图保证 ownProps
的一致性。在版本 7 中,这是通过在 connect()
中内部使用自定义的 Subscription
类来实现的,该类形成了一个嵌套的层次结构。这确保了树中较低级别的连接组件只有在最近的连接祖先更新后才会收到存储更新通知。但是,这依赖于每个 connect()
实例覆盖内部 React 上下文的一部分,为其提供自己的唯一 Subscription
实例以形成该嵌套,并使用该新的上下文值渲染 <ReactReduxContext.Provider>
。
使用 hooks,无法渲染上下文提供者,这意味着也没有嵌套的订阅层次结构。因此,在依赖于使用 hooks 而不是 connect()
的应用程序中,可能会重新出现“陈旧的 props”和“僵尸子组件”问题。
具体来说,“陈旧的 props”是指任何以下情况:
- 选择器函数依赖于此组件的 props 来提取数据
- 父组件将由于操作而重新渲染并传递新的 props
- 但此组件的选择器函数在该组件有机会使用这些新的 props 重新渲染之前执行
根据使用的 props 和当前存储状态,这可能会导致选择器返回不正确的数据,甚至抛出错误。
“僵尸子组件”专门指以下情况:
- 多个嵌套的连接组件在第一次传递中被挂载,导致子组件在父组件之前订阅存储
- 发送一个从存储中删除数据的动作,例如删除一个待办事项
- 结果,父组件将停止渲染该子组件
- 但是,由于子组件先订阅,它的订阅在父组件停止渲染它之前运行。当它根据 props 从存储中读取值时,该数据不再存在,如果提取逻辑不够谨慎,这可能会导致抛出错误。
useSelector()
尝试通过捕获所有由于存储更新而执行选择器时抛出的错误(但在渲染期间执行时除外)来处理这种情况。当发生错误时,组件将被迫重新渲染,此时选择器将再次执行。只要选择器是一个纯函数,并且您不依赖于选择器抛出错误,这就可以正常工作。
如果您希望自己处理这个问题,以下是一些使用 useSelector()
完全避免这些问题的可能选项
- 不要在选择器函数中依赖 props 来提取数据
- 在您确实依赖于选择器函数中的 props 并且这些 props 可能会随着时间的推移而改变,或者您要提取的数据可能基于可以删除的项目的情况下,尝试防御性地编写选择器函数。不要直接访问
state.todos[props.id].name
- 首先读取state.todos[props.id]
,并在尝试读取todo.name
之前验证它是否存在。 - 由于
connect
将必要的Subscription
添加到上下文提供者,并将评估子订阅推迟到连接的组件重新渲染后,在使用useSelector
的组件正上方的组件树中放置一个连接的组件将防止这些问题,只要连接的组件由于与钩子组件相同的存储更新而重新渲染。
有关这些场景的更详细描述,请参阅
性能
如前所述,默认情况下,useSelector()
在动作分派后运行选择器函数时,将对选定值的引用相等性进行比较,并且只有在选定值发生变化时才会导致组件重新渲染。但是,与 connect()
不同,useSelector()
不会阻止组件由于其父组件重新渲染而重新渲染,即使组件的 props 没有改变。
如果需要进一步的性能优化,您可以考虑将函数组件包装在 React.memo()
中
const CounterComponent = ({ name }) => {
const counter = useSelector((state) => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}
export const MemoizedCounterComponent = React.memo(CounterComponent)
Hooks 食谱
我们已经从最初的 alpha 版本中精简了我们的 hooks API,专注于更少的 API 原语集。但是,您可能仍然希望在自己的应用程序中使用我们尝试过的一些方法。这些示例应该可以随时复制粘贴到您自己的代码库中。
食谱:useActions()
这个 hook 在我们最初的 alpha 版本中,但在 v7.1.0-alpha.4
中被移除,基于 Dan Abramov 的建议。该建议是基于“绑定动作创建者”在基于 hooks 的用例中没有那么有用,并且会导致过多的概念开销和语法复杂性。
您可能更倾向于在您的组件中调用 useDispatch
hook 来获取对 dispatch
的引用,并在需要时手动在回调和效果中调用 dispatch(someActionCreator())
。您也可以在自己的代码中使用 Redux bindActionCreators
函数来绑定动作创建者,或者“手动”绑定它们,例如 const boundAddTodo = (text) => dispatch(addTodo(text))
。
但是,如果您想自己使用这个 hook,这里有一个可复制粘贴的版本,它支持将动作创建者作为单个函数、数组或对象传递。
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)
}
使用 hooks 时的其他注意事项
在决定是否使用 hooks 时,需要考虑一些架构上的权衡。Mark Erikson 在他的两篇博客文章中很好地总结了这些内容:关于 React Hooks、Redux 和关注点分离的思考 和 Hooks、HOC 和权衡。