Connect: Extracting Data with mapStateToProps
- mapStateToProps 返回 state 中所需的最少数据
- mapStateToProps 的返回值变化,组件会重新渲染
- mapStateToProps可以重新组装数据
- mapStateToProps 不应做耗时操作。耗时操作可以尝试在 action creator 、reduce 、render 中实现,如果确实需要在 mapStateToProps 中实现,可以考虑使用 可记忆的 selector, 例如 reselect
- 如果需要返回引用对象,一定返回新对象
- 也可以返回函数 (state, [props]) => object ,此时每一个 connect 的实例对象,都会调用一次 makeMapStateToProps 函数
Connect: Dispatching Actions with mapDispatchToProps
- 不传这个参数,默认会将
dispatch
添加到 props 中。如果传了,dispatch
默认不会传入 props 中。如果需要,需显式返回。 - mapDispatchToProps 支持 Function 和 Object 两种形式。推荐使用 plain object ,除非有特殊情况。
- 不建议在 component 中直接调用 store.dispatch 或者通过 context 直接获取 dispatch
函数形式:
const increment = () => ({ type: 'INCREMENT' })
const decrement = () => ({ type: 'DECREMENT' })
const reset = () => ({ type: 'RESET' })
const mapDispatchToProps = dispatch => {
return {
// dispatching actions returned by action creators
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement()),
reset: () => dispatch(reset())
}
}
bindActionCreators 形式:
import { bindActionCreators } from 'redux'
// ...
function mapDispatchToProps(dispatch) {
return bindActionCreators({ increment, decrement, reset }, dispatch)
}
// component receives props.increment, props.decrement, props.reset
connect(
null,
mapDispatchToProps
)(Counter)
Plain Object 形式:
当每一个字段都是对象的时候,redux 会假定这个对象就是一个 action creator。但是此时 props 中就没有 dispatch 了。
import {increment, decrement, reset} from "./counterActions";
const actionCreators = {
increment,
decrement,
reset
}
export default connect(mapState, actionCreators)(Counter);
// or
export default connect(
mapState,
{ increment, decrement, reset }
)(Counter);
如何获取 Store
- 使用 useStore Hook
- 理解 Context 的使用
react-redux 是通过 react 的 context 特性实现向深层嵌套的组件传递 redux store 的。React redux version 6 中,context 是 ReactReduxContext ,由 React.createContext() 创建。
<Provider> 实际是 <ReactReduxContext.Provider>
connect 实际是用 <ReactReduxContext.Consumer> 获取数据 - 自定义 context
- 多个 store
- 直接使用 ReactReduxContext
import { ReactReduxContext } from 'react-redux'
// in your connected component
function MyConnectedComponent() {
return (
<ReactReduxContext.Consumer>
{({ store }) => {
// do something useful with the store, like passing it to a child
// component where it can be used in lifecycle methods
}}
</ReactReduxContext.Consumer>
)
}
-----------API
connect
可以自定义 compare 函数,默认都是浅比较
{
context?: Object,
pure?: boolean,
areStatesEqual?: Function,
areOwnPropsEqual?: Function,
areStatePropsEqual?: Function,
areMergedPropsEqual?: Function,
forwardRef?: boolean,
}
Provider
connectAdvanced
connect 的一个底层实现,一般用不到。
batch()
组合多个 store 的改变在一个事件循环中,所以 UI 只重绘一次
import { batch } from 'react-redux'
function myThunk() {
return (dispatch, getState) => {
// should only result in one combined re-render, not two
batch(() => {
dispatch(increment())
dispatch(increment())
})
}
}
Hooks
- useSelector
- 从 React Redux v7 开始,由于使用了 batch behavior ,在同一个组件中一个 action 导致的多个 useSelector 只会导致一次重绘。
- useSelector 应执行快,避免耗时操作
- 具有缓存机制,比较机制是 ===
- 可以手动传入比较机制
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
- 配合 reselect 使用时,当一个 selector 只在一个组件中使用时,确保 selector 是同一个实例;当 selector 要在多个组件或者同个组件的多个实例中使用时,确保 selector 是多个实例。因为 reselect 的 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} />
</>
)
}
- useDispatch
- 只要 store 对象不变,useDispatch 的返回值 dispatch 就不会变。一般来说,在应用中,是不变的。但 React hooks lint rules 不知道,所以 dependency 里面可以加上 dispatch
- 一个利用 useMemo 的优化示例
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>
))
这个例子中,React.useMemo 的使用可以避免 onIncrement 的变化导致 MyIncrementButton 的重绘。
- useStore
这应该尽量少用,优先考虑 useSelector - 自定义 context 的相关 hooks 的使用
createStoreHook(MyContext)
createDispatchHook(MyContext)
createDispatchHook(MyContext)
警告
useSelector 可能会存在 Stale Props and "Zombie Children" 的问题。(旧的 Props 和 僵尸子节点)
性能
使用 connect() 的组件,如果 state 和 props 都没有改变,即使父组件重新渲染,子组件也不会重新渲染。但是 useSelector 的函数组件,子组件会随父组件一起重新渲染,即使 state 和 props 都没有改变。
为了更好的性能优化,我们可以使用 React.memo 来解决这个问题:
const CounterComponent = ({ name }) => {
const counter = useSelector(state => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}
export const MemoizedCounterComponent = React.memo(CounterComponent)
Hooks Recipes
- useActions:bindActionCreators 在函数组件中的替代,需要自己复制黏贴定义。
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)
}