[译] React-Redux 官方 Hooks 文档说明

Hooks

React的新 "hooks" APIs 赋予了函数组件使用本地组件状态,执行反作用,等各类能力。html

React Redux 如今提供了一系列 hook APIs 做为如今 connect() 高阶组件的替代品。这些 APIs 容许你,在不使用 connect() 包裹组件的状况下,订阅 Redux 的 store,和 分发(dispatch) actions。react

这些 hooks 首次添加于版本 v7.1.0。git

在一个 React Redux 应用中使用 hooks

和使用 connect() 同样,你首先应该将整个应用包裹在 <Provider> 中,使得 store 暴露在整个组件树中。github

const store = createStore(rootReducer)

ReactDOM.render(
  <Provider store={store}> <App /> </Provider>,
  document.getElementById('root')
)
复制代码

而后,你就能够 import 下面列出的 React Redux hooks APIs,而后在函数组件中使用它们。redux

useSelector()

const result : any = useSelector(selector : Function, equalityFn? : Function)
复制代码

经过传入 selector 函数,你就能够从从 Redux 的 store 中获取 状态(state) 数据。api

警告: selector 函数应该是个纯函数,由于,在任意的时间点,它可能会被执行不少次。数组

从概念上讲,selector 函数与 connectmapStateToProps 的参数是差很少同样的。selector 函数被调用时,将会被传入Redux store的整个state,做为惟一的参数。每次函数组件渲染时, selector 函数都会被调用。useSelector()一样会订阅 Redux 的 sotre,而且在你每 分发(dispatch) 一个 action 时,都会被执行一次。缓存

尽管如此,传递给 useSelector() 的各类 selector 函数仍是和 mapState 函数有些不同的地方:bash

  • selector 函数能够返回任意类型的值,并不要求是一个 对象(object)。selector 函数的返回值会被用做调用 useSelector() hook 时的返回值。
  • 当 分发(dispatch) 了一个 action 时,useSelector() 会将上一次调用 selector 函数结果与当前调用的结果进行引用(===)比较,若是不同,组件会被强制从新渲染。若是同样,就不会被从新渲染。
  • selector 函数不会接收到 ownProps 参数。可是 props 能够经过闭包获取使用(下面有个例子) 或者 经过使用柯里化的 selector 函数。
  • 当使用 记忆后(memoizing) 的 selectors 函数时,须要一些额外的注意(下面有个例子帮助了解)。
  • useSelector() 默认使用严格比较 === 来比较引用,而非浅比较。(看下面的部分来了解细节)

译者注: 浅比较并非指 ==。严格比较 === 对应的是 疏松比较 ==,与 浅比较 对应的是 深比较闭包

警告: 在 selectors 函数中使用 props 时存在一些边界用例可能致使错误。详见本页的 使用警告 小节。

你能够在一个函数组件中屡次调用 useSelector()。每个 useSelector() 的调用都会对 Redux 的 store 建立的一个独立的 订阅(subscription)。因为 Redux v7 的 批量更新(update batching) 行为,对于一个组件来讲,若是一个 分发后(dispatched) 的 action 致使组件内部的多个 useSelector() 产生了新值,那么仅仅会触发一次重渲染。

相等比较(Equality Comparisons) 和更新

当一个函数组件渲染时,传入的 selector 函数会被调用,其结果会做为 useSelector() 的返回值进行返回。(若是 selector 已经执行过,且没有发生变化,可能会返回缓存后的结果)

无论怎样,当一个 action 被分发(dispatch) 到 Redux store 后,useSelector() 仅仅在 selector 函数执行的结果与上一次结果不一样时,才会触发重渲染。在版本v7.1.0-alpha.5中,默认的比较模式是严格引用比较 ===。这与 connect() 中的不一样, connect() 使用浅比较来比较 mapState 执行后的结果,从而决定是否触发重渲染。这里有些建议关于如何使用useSelector()

对于 mapState 来说,全部独立的状态域被绑定到一个对象(object) 上返回。返回对象的引用是不是新的并不重要——由于 connect() 会单独的比较每个域。对于 useSelector() 来讲,返回一个新的对象引用老是会触发重渲染,做为 useSelector() 默认行为。若是你想得到 store 中的多个值,你能够:

  • 屡次调用 useSelector(),每次都返回一个单独域的值

  • 使用 Reselect 或相似的库来建立一个记忆化的 selector 函数,从而在一个对象中返回多个值,可是仅仅在其中一个值改变时才返回的新的对象。

  • 使用 React-Redux shallowEqual 函数做为 useSelector()equalityFn 参数,如:

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

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
复制代码

这个可选的比较函数参数使得咱们可使用 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>
}
复制代码

使用记忆化的 selectors 函数

当像上方展现的那样,在使用 useSelector 时使用单行箭头函数,会致使在每次渲染期间都会建立一个新的 selector 函数。能够看出,这样的 selector 函数并无维持任何的内部状态。可是,记忆化的 selectors 函数 (经过 reselect 库中 的 createSelector 建立) 含有内部状态,因此在使用它们时必须当心。

当一个 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 /> </> ) } 复制代码

这种作法一样适用于依赖组件 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} />
    </>
  )
}
复制代码

若是, 你想要在多个组件实例中使用相同的依赖组件 props 的 selector 函数,你必须确保每个组件实例建立属于本身的 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} />
    </>
  )
}
复制代码

被移除的:useActions()

useActions() 已经被移除

useDispatch()

const dispatch = useDispatch()
复制代码

这个 hook 返回 Redux store 的 分发(dispatch) 函数的引用。你也许会使用来 分发(dispatch) 某些须要的 action。

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 函数将回调函数记忆化,防止由于回调函数引用的变化致使没必要要的渲染。

译者注:这里的建议其实和 dispatch 不要紧,不管是否使用 dispatch,你都应该确保回调函数不会无端变化,而后致使没必要要的重渲染。之因此和 dispatch 不要紧,是由于,一旦 dispatch 变化,useCallback 会从新建立回调函数,回调函数的引用铁定发生了变化,然而致使没必要要的重渲染。

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> )) 复制代码

useStore()

const store = useStore()
复制代码

这个 hook 返回传递给 组件的 Redux sotore 的引用。

这个 hook 也许不该该被常用。 你应该将 useSelector() 做为你的首选。可是,在一些不常见的场景下,你须要访问 store,这个仍是有用的,好比替换 store 的 reducers。

例子

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

export const CounterComponent = ({ value }) => {
  const store = useStore()

  // 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()}</div>
}
复制代码

自定义 context

<Provider> 组件容许你经过 context 参数指定一个可选的 context。在你构建复杂的可复用的组件时,你不想让你本身的私人 store 与使用这个组件的用户的 Redux store 发生冲突,这个功能是颇有用的,

经过使用 hook creator 函数来建立自定义 hook,从而访问可选的 context。

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>
  )
}
复制代码

使用警告

过时 Props 和 "丧尸子组件"

有关 React Redux 实现一个难点在于,当你以 (state, ownProps) 形式定义 mapStateToProps 函数时,怎么保证每次都以最新的 props 调用 mapStateToProps。version 4 中,在一些边缘状况下,常常发生一些bug,好比一个列表中的某项被删除时, mapState 函数内部会抛出错误。

从 version 5 开始,React Redux 试图保证 ownProps 参数的一致性。在 version 7 中,经过在 connect() 内部使用一个自定义的 Subscription 类,实现了这种保证,也致使了组件被层层嵌套的形式。这确保了组件树深处 connect() 后的组件,只会在离本身最近的 connect() 后的祖先组件更新后,才会被通知 store 更新了。可是,这依赖于每一个 connect() 的实例副高 React 内部部分的 context,随后 connect() 提供了本身独特的 Subscription 实例,将组件嵌套其中,提供一个新的 conext 值给 <ReactReduxContext.Provider>,再进行渲染。

使用 hooks,意味着没法渲染 <ReactReduxContext.Provider>,也意味着没有嵌套的订阅层级。所以,“过时 Props” 和 "丧尸子组件" 的问题可能再次发生在你使用 hooks 而非 connect() 应用中。

详细的说,“过时 Props”可能发生的情况在于:

  • 某个 selector 函数依赖组件的 props 来取回数据。
  • 在某个 action 分发后,父组件将会重渲染而后传递新的props给子组件
  • 可是子组件的 selector 函数在子组件以新props渲染前,先执行了。

取决于使用的 props 和 stroe 当前的 状态(state) 是什么,这可能致使返回不正确的数据,甚至抛出一个错误。

"丧尸子组件" 特别指代下面这种状况:

  • 在刚开始,多个嵌套 connect() 后的组件一块儿被挂载,致使子组件的订阅先于其父组件。

  • 一个 action 被 分发(dispatch) ,删除了 store 中的某个数据,好比某个待作事项。

  • 父组件会中止渲染对应的子组件

  • 可是,由于子组件的订阅先于父组件,其订阅时的回调函数的运行先于父组件中止渲染子组件。当子组件根据props取回对应的数据时,这个数据已经不存在了,并且,若是取回数据代码的逻辑不够当心的话,可能会致使一个错误被抛出。

useSelector() 经过捕获全部 selector 内部由于 store 更新抛出的错误(但不包括渲染时更新致使的错误),来应对"丧尸子组件"的问题。当产生了一个错误时,组件会被强制重渲染,此时,selector 函数会从新执行一次。注意,只有当你的 selector 函数是纯函数且你的代码不依赖于 selector 抛出的某些自定义错误时,这个应对策略才会正常工做。

若是你更想要本身处理这些问题,这里有一些建议,在使用 useSelector() 时,可能帮助你避免这些问题。

  • 在 selector 函数不要依赖 props 来取回数据。

  • 对于你必需要依赖props,并且props常常改变的状况,以及,你取回的数据可能被删除的状况下,试着带有防护性的 selector 函数。不要直接取回数据,如:state.todos[props.id].name - 先取回 state.todos[props.id],而后检验值是否存在,再尝试取回 todo.name

  • 由于 connect 增添了必要 Subscription 组件给 context provider,且延迟子组件订阅的执行,一直到 connect() 的组件重渲染后,在组件树中,将一个 connect() 的组件置于使用了 useSelector 的组件之上,将会避免上述的问题,只要 connect() 的组件和使用了 hooks 子组件触发重渲染是由同一个 store 更新引发的。

注意:若是你想要这个问题更详细的描述,这个聊天记录详述了这个问题,以及 issue #1179.

性能

正如上文提到的,在一个 action 被分发(dispatch) 后,useSelector() 默认对 select 函数的返回值进行引用比较 ===,而且仅在返回值改变时触发重渲染。可是,不一样于 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 的建议下被移除了。建议代表了在使用 hook 的场景下,“对 action creators 进行绑定”没之前那么有用,且会致使更多概念上理解负担和增长语法上的复杂度。

译者注:action creators 即用来生成 action 对象的函数。

在组件中,你应该更偏向于使用 useDispatch hook 来得到 dispatch 函数的引用,而后在回调函数中手动的调用 dispatch(someActionCreator()) 或某种须要的反作用。在你的代码中,你仍然可使用bindActionCreators 函数绑定 action creators,或手动的绑定它们,好比 const boundAddTodo = (text) => dispatch(addTodo(text))。

可是,若是你本身想要使用这个 hook,这里有个 复制便可用 的版本,支持将 action creators 做为一个独立函数、数组、或一个对象传入。

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)
}
复制代码