React-Redux Hooks 中文

2021-06-07  本文已影响0人  ScottStone

原文:https://react-redux.js.org/api/hooks

翻译水平有待提高,虚心接受各位看官的指教,欢迎大家留言自己的见解

目录

1.在React Redux应用中使用Hooks
2.useSelector()
3.useDispatch()
4.useStore()
5.自定义 context
6.注意事项
7.Hooks 示例

简介

React的新特性 Hooks让函数组件可以使用类似Class组件的State等执行副作用。React让我们还可以自定Hooks,自定义的Hooks可以在React自带的Hooks之上抽离可以复用的操作。

React Redux 集成了自己定义的Hooks,这些Hooks可以让你的React组件订阅Redux store 和发送action

我们推荐使用React-Redux Hooks 作为React组件的默认实现方式。
已经存在的connect函数仍然可以使用并且会继续提供支持,但是Hooks更加简单,与TypeScript使用效果会更好。

这些Hooks函数支持的最低版本 :7.1.0。

1.在React Redux应用中使用Hooks#

使用 connect(),开始你需要把整个应用包裹在 <Provider> 组件中,确保store可以被整个组件树访问到:

const store = createStore(rootReducer)

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

在这之后,你就可以引用所有的React Redux Hooks 并可以在函数组件中使用它们。

2.useSelector()#

const result: any =  useSelector(selector:  Function, equalityFn?:  Function)

通过selector可以让你从Redux store state中获取到数据。

INFO
selector应该是纯函数,因为它可能会在任何时间点被执行多次。

selector概念上基本相当于 connect里的参数 mapStateToProps 。当函数组件渲染时selector就会执行(除非selector本身相较于之前组件渲染时没有变化,这时候Hook就会返回缓存的结果而不是重新运行selector)。 useSelector()也会订阅 Redux store,当action执行后会执行selector。

然而,selector跟 useSelector()mapState还是有很多不同:

INFO
有些在selector里使用props导致问题的情况,参照本文注意事项章节获取更多细节。

在一个函数组件中,你可能会调用多次 useSelector()。每次调用useSelector()都会对Redux store创建一个独立的订阅。因为React Redux v7版本中React 的更新是批处理的,在同一个组件中一个action的分发本来应该只会导致一次重新渲染,但却会引起多个useSelector()返回新的值。

比较与更新#

当一个函数组件渲染时,提供的selector函数会执行,结果也会通过useSelector()返回(如果是组件中的同一个函数的实例,则不会重新执行selector,而是通过hook返回之前缓存的结果)。

当一个aciton分发到Redux store后,useSelector()只会在selector结果与之前结果不同时才会强制重新渲染。v7.1.0-alpha.5版本,默认的比较方式是严格的 ===强比较。这个跟connect()不同,connect()只是浅比较了mapState的结果来决定是否需要重新渲染。这可能会影响你如何使用useSelector()

使用mapState,所有的变量都会通过一个组合的object返回。这并不关心返回的object是否是新的值——connect()只比较object中的每个变量。

使用 useSelector(),返回一个新的object默认肯定会重新渲染。如果你想从store中得到多个值,你可以:

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

使用 memoizing selectors#

如上面展示的,在内联 selector中使用 useSelector时,当组件渲染完后会创建一个新的selector实例。这个实例会 一直有效直到不再持有任何state。 当然,memoizing selector (比如 在 reselect中通过 createSelector创建) 确实会有一个内部的state,使用它们时必须注意。下面你会看到一些 memoizing selector的典型应用场景。

当selector只依赖于state时,仅需要确保它是在组件外声明的,这样同一个selector就可以被每次渲染使用:

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

如果selector依赖于组件的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} />
    </>
  )
}

但是,当selector被多个组件实例使用并且依赖组件的props时,你需要确定每一个组件的实例都能得到他自己的那个selector实例参见这里 ,获取进一步解释,为什么这是必须的:

import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const makeSelectCompletedTodosCount = () =>
  createSelector(
    (state) => state.todos,
    (_, completed) => completed,
    (todos, completed) =>
      todos.filter((todo) => todo.completed === completed).length
  )

export const CompletedTodosCount = ({ completed }) => {
  const selectCompletedTodosCount = useMemo(makeSelectCompletedTodosCount, [])

  const matchingCount = useSelector((state) =>
    selectCompletedTodosCount(state, completed)
  )

  return <div>{matchingCount}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <CompletedTodosCount completed={true} />
      <span>Number of unfinished todos:</span>
      <CompletedTodosCount completed={false} />
    </>
  )
}

3.useDispatch()#

const dispatch =  useDispatch()

useDispatch()dispatch返回了一个Redux store实例。你可以用它来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来memoize它。子组件可以通过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>
))

INFO
只要通过<Provider>传递同一个store实例,dispatch 函数就是稳定的。一般一个应用中store实例不会改变。
当然, React hook的检测规则不会知道 dispatch是否应该稳定,并且会警告 dispatch变量应该添加到useEffectuseCallback的依赖数组中。最简单的解决办法如下:

export const Todos() = () => {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchTodos())
  // Safe to add dispatch to the dependencies array
  }, [dispatch])
}

4.useStore()#

const store =  useStore()

useStore返回了与传递给 <Provider>一样的Redux store实例。

useStore不建议频繁使用。建议把useSelector() 作为首选方式。

当然,这个可能对个别需要使用store场景比较有用,比如代替 reducer。

示例#

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

5.自定义 context#

<Provider>组件可以让你通过context属性,指定一个自定义的context。如果你创建一个可复用的复杂组件这个设置会很有用,可以避免自定义应用内的Redux store冲突。

通过Hooks使用可替换的 context,可以使用hook创建函数:

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

6.注意事项#

无用Props 和僵尸子节点#

INFO
React-Redux Hooks 从v7.1.0版本开始可以稳定使用了。我们推荐使用Hooks作为组件的默认实现方式。但是也有些边界用例问题出现,我们记录了这些问题以便大家能知道这些问题。

React Redux在实现的时候最复杂的一个操作是需要保证mapStateToProps函数结构类似 (state, ownProps)这样,每次props更新后都会调用。从版本4.0开始就有反复出现了一些bug,比如:mapState函数因列表的item数据被删除了而抛出异常。

从5.0版本开始,React Redux试图通过ownProps来确保一致性。在7.0版本,在connect()内部通过自定义的Subscription来实现,形成了嵌套调用。这样确保了在组件树子节点的组件只会在最近的链接的父节点更新后才会收到store更新的通知。但是这依赖于每个connect()实例重写部分内部React context,用新的context支持自己独有的Subscription来组建这个嵌套调用,使用心得context作为 <ReactReduxContext.Provider>的参数。

使用Hooks,不会渲染context provider,也就是不会有嵌套层级的订阅。基于此,应用如果使用Hook代替connect()可能会复现无用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.
依赖于调用了什么样的props跟什么样的store state,这会导致selector返回不正确的数据,甚至抛出异常。

僵尸子组件确切的说是指如下情况:

useSelector()就是用来解决以上问题的:通过捕获所有由于store更新(不是在渲染期间执行)而导致selector运行产生的异常。只要selector是一个纯函数并且不依赖于selector抛出异常这个就会有效。

如果你倾向于自己处理这个问题,下面是些可行的使用useSelector()时避免上述问题的方案:

INFO
关于此场景更多的描述如下:

性能#

如早先提到的,使用默认的useSelector()当action执行后运行selector函数会对选中的值进行比较,只有当选中的值改变时组件才会重新渲染。但是不像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)

7.Hooks 示例#

基于初始的alpha release,我们对Hooks的API进行了精简。更关注较小的API原语。但是可能在你的应用中你仍然想使用一些我们试过的方式。下面的示例代码可以直接复制并粘贴到你自己的代码中。

方法: useActions()#

useActions()在之前的release分支中,但是基于Dan Abramov 的建议](https://github.com/reduxjs/react-redux/issues/1252#issuecomment-488160930)在v7.1.0-alpha.4版本移除掉了。这个建议是基于"binding action creators"在基于hook的使用场景下没有用,并且会导致很多概念上的成本与句法的复杂性。

你可能更倾向于调用 useDispatch 在你的组件中获取dispatch的实例。然后根据需要在回调或者effect中手动调用dispatch(someActionCreator()) 。你的代码中还可以使用Redux bindActionCreators 函数来绑定action creator,或者像const boundAddTodo = (text) => dispatch(addTodo(text))一样绑定它们。

如果你确实喜欢使用Hooks,下面是可以复制粘贴的版本,支持通过action creator传递函数、数组或者
对象。

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在他发表的两篇博客中总结的很好。 Thoughts on React Hooks, Redux, and Separation of ConcernsHooks, HOCs, and Tradeoffs

上一篇下一篇

猜你喜欢

热点阅读