原文: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
还是有很多不同:
- selector可以把任何值作为结果返回,不只是object。selector的返回值会被作为
useSelector()
的返回值。 - 当一个action执行后,
useSelector()
会把之前selector的值与当前的值做比较。 - selector 函数不会接收
ownProps
参数。然而,props可以通过闭包使用(看下面的例子),或者通过柯里化的selector使用。 - 当使用memoizing selector时需要特别注意(看下面的例子)。
-
useSelector()
默认使用严格的===
做比较,不是通过浅比较(看下面的例子)。
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中得到多个值,你可以:
多次调用
useSelector()
,每次返回一个变量的值。使用Reselect或者是类似的库,创建一个memoizing的selector,可以通过一个object返回多个值。仅在一个值改变后就返回一个新的object。
使用React-Redux 的浅比较函数作为
useSelector()
的参数equalityFn
。useSelector()
使用React-Redux中的浅比函数作为比较函数,比如:
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
变量应该添加到useEffect
和useCallback
的依赖数组中。最简单的解决办法如下:
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意味着:
- selector函数依赖这个组件的props提取数据。
- 父组件会重新渲染并且会以props的形式传递action的结果。
- 组件的selector函数,在组建接受到新的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返回不正确的数据,甚至抛出异常。
僵尸子组件确切的说是指如下情况:
- 首次加载多个嵌套的关联的组建会导致子组件在父组件之前订阅store信息。
- action会从store中返回删除的数据,比如todo item。
- 父组件会停止渲染子组件。
- 因为子组件先订阅了store信息,子组件的订阅会在父组件停止渲染它之前运行。当读取到store基于props的值时,如果数据不存在了,并且获取逻辑不严谨,可能会导致抛出异常。
useSelector()
就是用来解决以上问题的:通过捕获所有由于store更新(不是在渲染期间执行)而导致selector运行产生的异常。只要selector是一个纯函数并且不依赖于selector抛出异常这个就会有效。
如果你倾向于自己处理这个问题,下面是些可行的使用useSelector()
时避免上述问题的方案:
- selector函数不要依赖props获取数据。
- 以防你的selector函数确实需要依赖于props,这些props可能会改变,或者你获取的数据可能依赖于会不会删除的item,selector函数要定义的保守一点。不要只是直接用
state.todos[props.id].name
,先获取state.todos[props.id]
,确保值存在再获取todo.name
。 - 因为
connect
在context provider中添加了必要的Subscription
并且延时执行子组件的订阅操作直到关联的组建重新渲染完。在关联组件因同一个store更新而重新渲染时,组件使用useSelector
之前把关联的组建放入组件树可以避免上述问题。
INFO
关于此场景更多的描述如下:
- "Stale props and zombie children in Redux" by Kai Hao
- this chat log that describes the problems in more detail
- issue #1179
性能#
如早先提到的,使用默认的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 Concerns 和 Hooks, HOCs, and Tradeoffs。