React-Redux是Redux官方提供的React绑定库,它使你的React组件可以从Redux store中读取数据,并可以向store分发(dispatch)action以更新数据。
本文整理时React Redux版本为v7.1。
1. 介绍
1.1 快速上手
React-Redux是Redux官方提供的React绑定库,它使你的React组件可以从Redux store中读取数据,并可以向store分发(dispatch)action以更新数据。
1.1.1 安装
安装React Redux 7.1需要React 16.8.3+。
在React应用中使用React Redux:
npm install react-redux
或
yarn add react-redux
1.1.2 Provider
React Redux提供了一个<Provider />组件,它使你的整个应用都可以访问到Redux Store:
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
1.1.3 connect()
React Redux提供了一个connect()函数,用于将组件和store连接起来。
通常,可以像下面这样调用connect:
import { connect } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'
// const Counter = ...
const mapStateToProps = (state /*, ownProps*/) => {
return {
counter: state.counter
}
}
const mapDispatchToProps = { increment, decrement, reset }
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
1.1.4 帮助与讨论
Reactiflux Discord社区的#redux 频道是官方提供的,可用于解决学习和使用Redux相关问题。
也可以在Stack Overflow的#redux 标签下,反馈问题或获取解决方案。
1.2 基础教程
以下将通过一todos示例来介绍React Redux的使用。
1.2.1 Todo 示例
跳转:
React UI 组件
本示例中将实现以下React UI 组件:
TodoApp应用的入口组件。其会渲染标题,及AddTodo、TodoList、和VisibilityFilters组件AddTodo用于用于添加todo项目,其包含一个输入框及一个“Add Toto”按钮:- 它使用受控输入,用户输入根据
onChange设置状态。 - 当用户单击“Add Todo”按钮时,它会分发动作(使用React Redux提供)将待办事项添加到store。
- 它使用受控输入,用户输入根据
TodoList用于渲染todos列表的组件:- 当选择
VisibilityFilters时,它将渲染todo的过滤列表
- 当选择
Todo渲染单个todo项的组件:- 它会渲染todo项的内容,并通过“删除线”来表示待办事项已完成。
- 它会在
onClick上分发操作(dispatch action)以切换待办事项的完整状态(state)。
VisibilityFilters呈现一组简单的过滤器:all、completed和incomplete.。点击其中之一时将会过滤todos:- 它接受来自父组件的
activeFilter属性(prop),该属性指示用户当前选择了哪个过滤器。活动的过滤器带有下划线。 - 它会分发s
setFilter操作(action)以更新所选过滤器。
- 它接受来自父组件的
constants保存应用程序中的常量数据- 最后,
index将应用渲染到DOM中
Redux Store
应用中 Redux 的部分已按Redux文档中推荐的模式进行了设置:
- Store
todos:todos的归一化reducer。它包含所有待办事项的byIds映射和包含所有ID列表的allIds。visibilityFilters:简单的字符串:all、completed和incomplete
- Action Creators
addTodo创建待办事项的create。其接受一个字符串变量content,并返回带有payload(其中包含id和content)的ADD_TODO类型的actiontoggleTodo创建一个切换todos的action。仅接受一个number类型的变量id,并返回带有payload(仅id)的TOGGLE_TODO类型的actionsetFilter创建用于设置当前应用筛选器的action, 并返回带有payload(包含filter自身)的TOGGLE_TODO类型的action。
- Reducers
todos的reducer- 将
id添加到其allIds字段,并在收到ADD_TODOaction后在其byIds字段中设置待办事项。 - 收到
TOGGLE_TODOaction后,切换待办事项的completed字段。
- 将
visibilityFilters reducer,在收到SET_FILTERaction时从其payload中接受新的过滤器。
- Action Types
- 通过
actionTypes.js文件来保存action类型常量
- 通过
- Selectors
getTodoList:从todos的store中返回allIds列表getTodoById:通过指定的id在store中查找代办事项getTodos:稍微复杂一点。 它从allIds中获取所有id,并在byIds中找到每个待办事项,然后返回待办事项的最终数组getTodosByVisibilityFilter:根据可见性过滤器过滤待办事项
可以在此CodeSandbox中找以上所说的React UI组件源码及未连接的Redux Store。
接下来,将介绍如何使用React Redux将store连接到我们的应用。
1. 提供 Store
首先,需要使store在应用中可访问,这可以通过React Redux提供的<Provider />API实现。
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoApp from './TodoApp'
import { Provider } from 'react-redux'
import store from './redux/store'
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<TodoApp />
</Provider>,
rootElement
)
可以看到,<TodoApp />是一个被<Provider />包裹的子组件,而store是做为一个prop被传入的。
2. 连接组件
React Redux提供了一个connect函数,可让你从Redux Store中读取值(并在store更新时重新读取值)。
connect函数有两个参数,均为可选参数:
mapStateToProps:每次store状态更改时调用。它接收整个store状态,并应返回此组件所需要的数据对象。mapDispatchToProps:本参数可以是函数,或对象- 如果是函数,则在创建组件时会调用一次。它将接收到
dispatch参数,并应返回一个对象,该对象具有使用dispatch进行操作调度(dispatch action)的功能。 - 如果它是一个由action creator组成的对象,则每个action creator都会被转换为prop函数,该函数会在调用时自动调度其action。 注意:建议使用“对象简写”形式。
- 如果是函数,则在创建组件时会调用一次。它将接收到
一般来说,可以像下面这样调用connect:
const mapStateToProps = (state, ownProps) => ({
// ... computed data from state and optionally ownProps
})
const mapDispatchToProps = {
// ... normally is an object full of action creators
}
// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(
mapStateToProps,
mapDispatchToProps
)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)
// We normally do both in one step, like this:
connect(
mapStateToProps,
mapDispatchToProps
)(Component)
首先,我们来研究下<AddTodo />。它需要触发对store的更改以添加新的待办事项,所以,它需要能够将actiondispatch给store。
以下是我们的addTodoaction creator:
// redux/actions.js
import { ADD_TODO } from './actionTypes'
let nextTodoId = 0
export const addTodo = content => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content
}
})
// ... other actions
通过将其传递给connect,我们的组件将其作为prop接收,并且它将在调用时自动调度action。
// components/AddTodo.js
// ... other imports
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ... component implementation
}
export default connect(
null,
{ addTodo }
)(AddTodo)
现在<AddTodo />会被名为<Connect(AddTodo)/>的父组件包裹。同时<AddTodo />会得到一个prop:addTodo action。
我们还需要实现handleAddTodo函数,以使其分发addTodo的action并重置输入:
// components/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ...
handleAddTodo = () => {
// dispatches actions to add todo
this.props.addTodo(this.state.input)
// sets state back to empty string
this.setState({ input: '' })
}
render() {
return (
<div>
<input
onChange={e => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
)
}
}
export default connect(
null,
{ addTodo }
)(AddTodo)
<TodoList />组件负责渲染待办事项列表。因此,它需要从store中读取数据。我们通过mapStateToProps参数调用connect来启用它,该函数描述了我们需要从store中获取数据的哪一部分。
<Todo />组件将待办事项作为props。可以从todos的byIds字段中获取此信息。但是,我们还需要来自store的allIds字段的信息,该信息指示应执行的待办事项和顺序。mapStateToProps函数看起来可能类似如下:
// components/TodoList.js
// ...other imports
import { connect } from "react-redux";
const TodoList = // ... UI component implementation
const mapStateToProps = state => {
const { byIds, allIds } = state.todos || {};
const todos =
allIds && allIds.length
? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
: null;
return { todos };
};
export default connect(mapStateToProps)(TodoList);
选择器(selector)可以做到这一点,简单地导入选择器并在此处使用它。
// redux/selectors.js
export const getTodosState = store => store.todos
export const getTodoList = store =>
getTodosState(store) ? getTodosState(store).allIds : []
export const getTodoById = (store, id) =>
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {}
export const getTodos = store =>
getTodoList(store).map(id => getTodoById(store, id))
// components/TodoList.js
// ...other imports
import { connect } from "react-redux";
import { getTodos } from "../redux/selectors";
const TodoList = // ... UI component implementation
export default connect(state => ({ todos: getTodos(state) }))(TodoList);
建议将所有复杂的查询或数据计算封装在选择器函数中。此外,可以通过使用Reselect编写可以跳过不必要操作的“记忆式”选择器,以进一步优化性能。
关于选择器的相关参考:
现在<TodoList />已连接到store。它应该接收待办事项列表,在它们上进行映射,然后将每个待办事项传递给<Todo />会将它们渲染到屏幕上。现在尝试添加待办事项,其应该出现在我们的待办事项列表上。
我们将连接更多组件。在此之前,让我们先暂停,介绍一些有关connect的知识。
3. 常用调用connect方法
根据使用组件的不同,调用connect方法也有所不同。最常用的方法总结如下:
| 不订阅Store | 订阅Store | |
|---|---|---|
| 不注入Action Creators | connect()(Component) |
connect(mapStateToProps)(Component) |
| 注入Action Creators | connect(null, mapDispatchToProps)(Component) |
connect(mapStateToProps, mapDispatchToProps)(Component) |
不订阅Store且不注入Action Creators
如果connect不传入任何参数,则组件:
- store发生修改时,不会重新渲染
- 接收可以用来手动分发action的
props.dispatch
// ... Component export default connect()(Component) // Component will receive `dispatch` (just like our!)
订阅Store但不注入Action Creators
如果connect仅传入mapStateToProps参数,则组件:
- 订阅
mapStateToProps从store中提取的值,并仅在这些值更改时重新渲染 - 接收可以用来手动分发action的
props.dispatch
// ... Component const mapStateToProps = state => state.partOfState export default connect(mapStateToProps)(Component)
不订阅Store注入Action Creators
如果connect仅传入mapDispatchToProps参数,则组件:
- store修改时不会重新渲染
- 接收使用
mapDispatchToProps注入的每个action creator作为props,并在被调用时自动分发action
import { addTodo } from './actionCreators'
// ... Component
export default connect(
null,
{ addTodo }
)(Component)
订阅Store且注入Action Creators
如果connect传入mapStateToProps和mapDispatchToProps参数,则组件:
- 订阅
mapStateToProps从store中提取的值,并仅在这些值已更改时重新渲染 - 接收使用
mapDispatchToProps注入的所有action creator作为props,并在被调用时自动分发action
以四种情况涵盖了connect的最基本用法。要了解有关connect的更多信息,请参阅:connectAPI。
接下来,继续连接<TodoApp />中的剩余组件。
我们应该如何实现todos的交互?你可能已经有了答案。我们将以类似的方式连接<Todo />以调度toggleTodo:
// components/Todo.js
// ... other imports
import { connect } from "react-redux";
import { toggleTodo } from "../redux/actions";
const Todo = // ... component implementation
export default connect(
null,
{ toggleTodo }
)(Todo);
现在,todos已经可以完成切换交互了。
最后,实现VisibilityFilters。
<VisibilityFilters />组件需要能够从store中读取当前处于活动状态的过滤器,并向store分发action。因此,我们需要同时传递mapStateToProps和mapDispatchToProps。其中,mapStateToProps可以是visibleFilter状态的简单访问器。而mapDispatchToProps将包含setFilter动作创建者(action creator)。
// components/VisibilityFilters.js
// ... other imports
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
const VisibilityFilters = // ... component implementation
const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);
同时,我们还需要更新<TodoList />组件以根据活动过滤器过滤待办事项。以前,传递给<TodoList />connect函数调用的mapStateToProps只是选择器,它选择了整个todos列表。让我们编写另一个选择器,以帮助根据状态过滤todos。
// redux/selectors.js
// ... other selectors
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
const allTodos = getTodos(store)
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter(todo => todo.completed)
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter(todo => !todo.completed)
case VISIBILITY_FILTERS.ALL:
default:
return allTodos
}
}
并在选择器的帮助下连接到store:
// components/TodoList.js
// ...
const mapStateToProps = state => {
const { visibilityFilter } = state
const todos = getTodosByVisibilityFilter(state, visibilityFilter)
return { todos }
}
export default connect(mapStateToProps)(TodoList)
以上,我们使用React Redux完成了一个非常简单的todos示例,所有组件己被连接。
1.2.2 相关链接
1.2.3 更多
- Reactiflux Redux 频道
- StackOverflow
- GitHub Issues
1.3 为什么使用React Redux
Redux本身是一个独立的库,可以与任何UI层或框架一起使用,包括:React、Angular、Vue、Ember和vanilla JS。尽管Redux和React通常一起使用,但它们彼此独立。
需要将Redux与任何类型的UI框架一起使用,通常使用“ UI绑定”库将Redux与UI框架绑定在一起,而不是通过UI代码直接与store进行交互。
React Redux是React的官方Redux UI绑定库。如果同时使用Redux和React,则还应该使用React Redux以绑定这两个库。
本节将介绍“UI绑定库”的作用,以了解为什么使用React Redux。
1.3.1 将Redux与UI集成
将Redux与任何UI层集成需要以下步骤:
- 创建一个Redux Store
- 订阅更新
- 在订阅回调中:
- 获取当前store状态
- 提取当前UI所需的数据
- 使用数据更新用户界面
- 如有必要,以初始渲染呈现UI
- 通过调度Redux Action来响应UI输入
虽然可以手工编写这些逻辑,但这样做会变得非常重复。另外,优化UI性能也会需要复杂的逻辑。
订阅store,检查更新的数据以及触发重新渲染的过程可以变得更加通用和可重用。UI绑定库(如:React Redux)处理store交互逻辑,使你可以不必自己写相关代码。
注意:深入了解React Redux内部如何工作以及如何处理商店交互,请参阅:Idiomatic Redux:React Redux的历史和实现。
1.3.2 使用React Redux的原因
React的官方Redux UI绑定库
虽然Redux可以与任何UI层一起使用,但其最初是设计用于React。许多其他框架都有UI绑定层,但是React Redux由Redux团队直接维护。
作为用于React的官方Redux绑定,React Redux会随着两个库中的任何API更改保持最新状态,以确保你的React组件的行为符合预期。它的预期用法采用React的设计原则:编写声明性组件。
更好的React体系结构
React组件非常类似函数。虽然可以在一个函数中编写所有代码,但通常最好将逻辑拆分成较小的函数,每个函数处理一个特定任务,以使其更易于理解。
同样,虽然可以编写处理许多不同任务的大型React组件,但通常最好根据职责来拆分组件。特别是,通常有负责收集和管理某种数据的“容器”组件,以及根据props接收的任何数据简单地显示UI的“展示”组件。
React Redux的connect函数生成“容器”包装器组件,这些组件为你处理与store交互的过程。这样,你自己的组件就可以专注于其他任务,无论是处理其他数据还是仅显示一部分UI。此外,connect可以抽象出正在使用哪个store,从而使你自己的组件有更好的重用性。
作为一般的架构原则,我们想让自己的组件“不知道” Redux。他们应该像其他任何React组件一样简单地接收数据和作为props的函数。最终,这会使测试和重用自己的组件变得更加容易。
性能优化
React通常很快,但是默认情况下,对组件的任何更新都会使React重新渲染组件树那部分内的所有组件。但如果给定组件的数据没有更改,重新渲染就可能会造成浪费一些,因为请求的UI输出是相同的。
如果比较关注性能,则提高性能的最佳方法是跳过不必要的重新渲染,以在数据实际更改时才重新渲染组件。React Redux在内部实现了许多性能优化,可以使你的组件仅在实际需要时才重新渲染。
另外,通过在React组件树中连接多个组件,可以确保每个连接的组件仅从该组件所需的store状态中提取特定的数据。这意味着你的组件将会减少重新渲染的频率,因为在大多数情况下,这些特定的数据并没有改变。
社区支持
作为官方绑定库,React Redux有宠大的用户社区。这样可以更轻松地获取帮助,了解使用基于React Redux库的最佳实践等。
1.3.3 相关引用及链接
理解 React Redux
社区资源
- #Reactiflux上的#redux (Reactiflux邀请链接 link)
- Stack Overflow 上相关主题: Redux, React Redux
- Reddit: /r/reactjs, /r/reduxjs
- Github issues (Bug提交与功能提交): https://github.com/reduxjs/react-redux/issues
- 教程、文章及相关资源:React/Redux Links
- DEV 社区: DEV Redux标签
2. 使用 React-Redux
2.1 Connect: 使用mapStateToProps提取数据
作为传递给connect的第一个参数,mapStateToProps用于从store中选择连接的组件所需的数据部分。通常简称为:mapState。
- 每次store状态(
state)更改时都会调用它 - 它接收整个store的状态,并应返回此组件需要的数据对象。
2.1.1 定义mapStateToProps
mapStateToProps需要定义为一个函数:
function mapStateToProps(state, ownProps?)
它应该接受一个称为state的第一个参数;可选的称为ownProps的第二个参数,并返回一个包含连接组件所需数据的普通对象。
此函数应作为connect的第一个参数传递,并且每次Reduxstore状态更改时都会调用此函数。如果你不想订阅store,请传递null或undefined以代替mapStateToProps传给connect。
不管是使用function关键字的形式定义(function mapState(state) { }),还是使用箭头函数(const mapState = (state) => { }),其工作方式相同。
参数
stateownProps(可选)
state
mapStateToProps函数的第一个参数是整个Redux存储的状态(与调用store.getState()返回的值相同)。因此,第一个参数传统上称为state。(尽管可以给参数指定任意名称,但将其称为store是错误的-它是“state值”,而不是“store实例”。)
mapStateToProps函数应至少传递state:
// TodoList.js
function mapStateToProps(state) {
const { todos } = state
return { todoList: todos.allIds }
}
export default connect(mapStateToProps)(TodoList)
ownProps(可选)
如果你的组件需要来自其自身属性数据以从store中检索数据,则可以使用第二个参数ownProps定义函数。此参数包含由connect生成的所有给包装器组件的支持。
// Todo.js
function mapStateToProps(state, ownProps) {
const { visibilityFilter } = state
const { id } = ownProps
const todo = getTodoById(state, id)
// component receives additionally:
return { todo, visibilityFilter }
}
// Later, in your application, a parent component renders:
;<ConnectedTodo id={123} />
// and your component receives props.id, props.todo, and props.visibilityFilter
一般不需要在mapStateToProps.connect返回的对象中包含ownProps的值。connect会自动将这些不同来源的prop合并为最终的props。
返回值
mapStateToProps函数应返回一个普通对象,其中包含组件所需的数据:
- 对象中的每个字段都将成为实际组件的prop
- 字段中的值将用于确定组件是否需要重新渲染
示例:
function mapStateToProps(state) {
return {
a: 42,
todos: state.todos,
filter: state.visibilityFilter
}
}
// component will receive: props.a, props.todos, and props.filter
2.1.2 使用指南
让mapStateToProps重构Store中的数据
mapStateToProps函数可以、而且应该做的不仅仅是return state.someSlice。它们负责根据组件的需要“重构”store数据。这可能包括:返回一个值作为特定的prop名称、组合来自状态树(state tree)不同部分的数据、以及以不同的方式转换store数据。
用选择器函数抽取和转换数据
强烈建议使用“选择器”(Selector)功能,以帮助封装从状态树中指定位置提取值。记忆化选择器在提升应用性能方面也起着关键作用(关于选择器相关介绍的更多详细信息,请参见:高级用法:性能)
mapStateToProps 函数应该足够快
每当store更改时,所有已连接组件的所有mapStateToProps函数都将运行。因此,你的mapStateToProps函数应尽可能快地运行。这也意味着比较慢的mapStateToProps函数可能是应用的潜在瓶颈。
作为“重构数据”想法的一部分,mapStateToProps函数经常需要以各种方式转换数据(例如,过滤数组、将ID数组映射到其对应的对象或从Immutable.js对象提取纯JS值)。这些转换通常在执行成本以及组件是否因此而重新渲染方面开销最高。如果考虑性能,应确保仅在输入值已更改的情况下运行这些转换。
mapStateToProps 函数应是原生和同步的
与Redux Reducer非常相似,mapStateToProps函数应始终为纯函数且同步。它应该仅将state(和ownProps)作为参数,并返回组件所需的数据作为props。不应将其用于触发异步行为,如:获取数据的AJAX调用,并且不应将函数声明为async。
2.1.3 mapStateToProps 和性能
返回值决定了组件是否重新渲染
React Redux在内部实现了shouldComponentUpdate方法,以便包装器组件在组件所需的数据发生更改时准确地重新渲染。默认情况下,React Redux使用===比较(“浅相等性”检查)在返回对象的每个字段上确定从shouldComponentUpdate返回的对象的内容是否不同。如果任何字段发生更新,则将重新渲染您的组件,以便它可以将更新值作为prop接收。请注意,返回相同引用的变异对象是一个常见错误,它可能会导致你的组件无法按预期重新渲染。
总结connect和mapStateToProps封装的组件的行为,以从store中提取数据:
(state) => stateProps |
(state, ownProps) => stateProps |
|
|---|---|---|
当mapStateToProps执行时: |
store的state发生更改 |
store的state发生更改 或 ownProps的任何字段有所不同 |
| 当组件重新渲染时: | stateProps的任何字段有所不同 |
stateProps的任何字段有所不同或 ownProps的任何字段有所不同 |
仅在需要时返回新对象引用
React Redux会进行浅比较,以查看mapStateToProps结果是否已更改。这很容易意外返回新的对象或数组引用,这样即使数据实际上相同,也会导致组件重新渲染。
许多常见的操作会导致创建新的对象或数组引用:
- 通过
someArray.map()或someArray.filter()创建新数组 - 通过
array.concat合并数组 - 通过
array.slice选择数组的一部分 - 通过
Object.assign复制值 - 通过展开运算符复制值
{ ...oldState, ...newData }
将这些操作放在记忆化选择器函数中,以确保它们仅在输入值已更改时才运行。这还会确保如果输入值未更改,则mapStateToProps仍将返回与以前相同的结果值,并且connect可以跳过重新渲染。
仅在数据修改时执行大开销操作
转换数据通常开销很大(会导致创建新的对象引用)。为了使mapStateToProps函数尽可能快,仅应在相关数据已更改时重新运行这些复杂的转换。
有几种方法可以解决这一问题:
- 可以在action creator或reducer计算一些转换,并且转换后的数据可以保存在store中
- 转换可以在组件的
render()方法中完成 - 如果确实需要在
mapStateToProps函数中完成转换,那么建议使用记忆化选择器函数以确保仅在输入值已更改时才运行转换。
2.1.4 行为与陷阱
mapStateToProps在Store State相同时不会执行
由connect生成的包装器组件订阅Redux store。每次调度action时,它都会调用store.getState()并检查lastState === currentState。如果两个状态值引用相同,则它将不会重新运行mapStateToProps函数,因为它假定其余store状态均未更改。
Redux combineReducers返回旧的状态对象,而不是新的状态对象。 这意味着reducer中的修改可能会导致根状态对象不被更新,因此UI不会重新渲染。
声明参数的数量会影响行为
仅使用(state)只要根store状态对象(state)不同,该函数就会运行。而使用(state, ownProps),它可以在store状态不同的时候运行,并且在包装器props发生更改时也会运行。
这意味着除非你实际需要使用它,否则不应该添加ownProps参数,否则的mapStateToProps函数将比所需的运行次数更多。
此行为还有些极端的情况。强制参数的数量确定mapStateToProps是否接收ownProps。
如果函数的形参包含一个强制性参数,则mapStateToProps将不会收到ownProps:
function mapStateToProps(state) {
console.log(state) // state
console.log(arguments[1]) // undefined
}
const mapStateToProps = (state, ownProps = {}) => {
console.log(state) // state
console.log(ownProps) // undefined
}
当函数的形参包含零个或两个强制性参数时,它将收到ownProps:
function mapStateToProps(state, ownProps) {
console.log(state) // state
console.log(ownProps) // ownProps
}
function mapStateToProps() {
console.log(arguments[0]) // state
console.log(arguments[1]) // ownProps
}
function mapStateToProps(...args) {
console.log(args[0]) // state
console.log(args[1]) // ownProps
}
2.1.5 相关引用及链接
教程:
性能:
Q&A:
- 为什么我的组件经常重新渲染?
- 为什么我的组件没有重新渲染,或者我的
mapStateToProps没有运行? - 如何加快
mapStateToProps? - 我应该只连接顶级组件,还是可以连接树中的多个组件?
2.2 Connect: 使用mapDispatchToProps分发action
- 2.2.1 分发(dispatch)的路径
- 2.2.2 两种形式的
mapDispatchToProps - 2.2.3 将
mapDispatchToProps定义为一个函数 - 2.2.4 将
mapDispatchToProps定义为一个对象 - 2.2.5 常见问题
- 2.2.6 相关引用及链接
作为传递给connect的第二个参数,mapDispatchToProps用于将action调度(dispatch)到store。
dispatch是Redux store的功能。调用store.dispatch调度一个action,是触发状态更改的唯一方法。
使用React Redux,你的组件永远不会直接访问store-connect会帮你完成。React Redux提供了两种组件调度动作的方式:
- 默认情况下,连接的组件会接收
props.dispatch并可以自己调度action。 connect可以接受一个名为mapDispatchToProps的参数,该参数可让你创建在调用时调度的函数,并将这些函数作为props传递给组件。
mapDispatchToProps函数通常简称为mapDispatch,但实际使用的变量名可以自定义为任何名称。
2.2.1 分发(dispatch)的路径
默认: dispatch 作为一个Prop
如果没有为connect()指定第二个参数,则默认情况下组件将收到dispatch。例如:
connect()(MyComponent) // which is equivalent with connect( null, null )(MyComponent) // or connect(mapStateToProps /** no second argument */)(MyComponent)
以这种方式连接组件后,组件将收到props.dispatch。可以使用它向store调度action。
function Counter({ count, dispatch }) {
return (
<div>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'RESET' })}>reset</button>
</div>
)
}
提供一个mapDispatchToProps参数
提供mapDispatchToProps使你可以指定组件可能需要调度的action。它使你可以提供action调度功能作为props。所以,可以直接调用props.increment(),而不是调用props.dispatch(() => increment())。可能需要这样做有以下几个原因。
更具说明性
首先,将调度逻辑封装到函数中可以使实现更具声明性。调度action并让Redux store处理数据流是如何实现行为,而不是行为。
一个很好的例子是单击按钮时调度一个action。从概念上讲,直接连接按钮可能没有任何意义,并且按钮引用dispatch也没有意义。
// button needs to be aware of "dispatch"
<button onClick={() => dispatch({ type: "SOMETHING" })} />
// button unaware of "dispatch",
<button onClick={doSomething} />
一旦用调度action的函数包装了我们所有的action creator,该组件就无需dispatch。因此,如果定义自己的mapDispatchToProps,则连接的组件将不再接收dispatch。
将Action调度逻辑传递给(未连接的)子组件
此外,还可以将action调度功能传递给子组件(可能是未连接的组件)。这允许更多组件调度action,同时使它们“不知道”Redux。
// pass down toggleTodo to child component
// making Todo able to dispatch the toggleTodo action
const TodoList = ({ todos, toggleTodo }) => (
<div>
{todos.map(todo => (
<Todo todo={todo} onClick={toggleTodo} />
))}
</div>
)
这就是React Redux的connect所做的:它封装了与Redux store进行通话的逻辑,而对你完全透明。这就是你应该在实现中完全利用的东西。
2.2.2 两种形式的mapDispatchToProps
mapDispatchToProps参数可以使用两种形式。其中,函数形式允许更多的自定义,但对象形式更易于使用。
- 函数形式:允许更多的自定义,可获取对
dispatch和ownProps(可选)的访问 - 对象简写形式:更具说明性,更易于使用
注意:更建议使用对象形式的mapDispatchToProps,除非特别需要以某种方式自定义调度行为。
2.2.3 将mapDispatchToProps定义为一个函数
将mapDispatchToProps定义为函数可以提供最大的灵活性,以自定义组件接收的函数和它们如何调度action。你可以访问dispatch和ownProps,并利用这个机会编写自定义的函数,以供所连接的组件调用。
参数
dispatchownProps(可选)
dispatch
mapDispatchToProps函数将dispatch作为第一个参数来调用。通常,你可以返回在自身内部调用dispatch()的新函数来利用此功能,或者直接传递简单的action对象,或者传递action creator的结果。
const mapDispatchToProps = dispatch => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' })
}
}
你可能还希望将参数转发给action creator:
const mapDispatchToProps = dispatch => {
return {
// explicitly forwarding arguments
onClick: event => dispatch(trackClick(event)),
// implicitly forwarding arguments
onReceiveImpressions: (...impressions) =>
dispatch(trackImpressions(impressions))
}
}
ownProps(可选)
如果你的mapDispatchToProps函数声明为带有两个参数,则会将dispatch作为第一个参数,并将props传递给连接组件作为第二个参数,还会在连接的组件收到新的props时重新调用它。
这意味着,不是在组件重新渲染现时将新的props重新绑定到action调度器,而是组件的props发生变化时。
// binds on component re-rendering
;<button onClick={() => this.props.toggleTodo(this.props.todoId)} />
// binds on `props` change
const mapDispatchToProps = (dispatch, ownProps) => {
toggleTodo: () => dispatch(toggleTodo(ownProps.todoId))
}
返回值
mapDispatchToProps函数应返回一个普通对象:
- 对象中的每个字段都将成为你自己组件的单独属性,并且该值通常应是在调用时调度action的函数。
- 如果您在
dispatch中使用action creator(与普通对象action相反),则习惯上只需将字段键命名为与action creator相同的名称: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()) } }
mapDispatchToProps函数的返回值将作为props合并到所连接的组件中。可以直接调用它们以调度其action。
function Counter({ count, increment, decrement, reset }) {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={reset}>reset</button>
</div>
)
}
CodeSandbox上的完整代码。
用bindActionCreators来定义mapDispatchToProps函数
手动包装这些功能很繁琐,因此Redux提供了一个简化功能。
bindActionCreators会将其值为action creator的对象转换为有相同键的对象,但每个action creator都会包装在一个dispatch调用中,以便可以直接调用它们。查看Redux上的bindActionCreators文档。
bindActionCreators接受两个参数:
- 一个
function(一个action creator)或一个对象(每个字段是一个action creator) dispatch
bindActionCreators生成的包装函数将自动转发其所有参数,因此无需手动执行此操作。
import { bindActionCreators } from 'redux'
const increment = () => ({ type: 'INCREMENT' })
const decrement = () => ({ type: 'DECREMENT' })
const reset = () => ({ type: 'RESET' })
// binding an action creator
// returns (...args) => dispatch(increment(...args))
const boundIncrement = bindActionCreators(increment, dispatch)
// binding an object full of action creators
const boundActionCreators = bindActionCreators(
{ increment, decrement, reset },
dispatch
)
// returns
// {
// increment: (...args) => dispatch(increment(...args)),
// decrement: (...args) => dispatch(decrement(...args)),
// reset: (...args) => dispatch(reset(...args)),
// }
要在mapDispatchToProps函数中使用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)
手工注入dispatch
如果提供了mapDispatchToProps参数,则该组件将不再接收默认的dispatch。可以将其手工添加到mapDispatchToProps的返回值中来将其恢复。在大多数情况下,并不需要这样做:
import { bindActionCreators } from 'redux'
// ...
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ increment, decrement, reset }, dispatch)
}
}
2.2.4 将mapDispatchToProps定义为一个对象
我们已经看到在React组件中调度Redux action的设置过程非常相似:定义一个action creator,将其包装在另一个类似(…args) => dispatch(actionCreator(…args))的函数中,然后 将包装函数作为props传递给组件。
由于这很常见,所以connect支持mapDispatchToProps参数的“对象简写”形式:如果传递的对象中包含action creator而不是函数,则connect会在内部自动调用bindActionCreators。
建议始终使用“对象简写”形式的mapDispatchToProps,除非有特殊的原因需要自定义调度行为。
请注意:
- 假设
mapDispatchToProps对象的每个字段都是action creator - 你的的组件将不再接收
dispatch作为props
// React Redux does this for you automatically: dispatch => bindActionCreators(mapDispatchToProps, dispatch)
因此,mapDispatchToProps可以简单地是:
const mapDispatchToProps = {
increment,
decrement,
reset
}
你可以自定义实际变量名,例如:actionCreators,甚至在connect 调用中内联所定义的对象:
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);
2.2.5 常见问题
为什么组件没有收到dispatch?
也称为:
TypeError: this.props.dispatch is not a function
这是当尝试调用this.props.dispatch时发生的常见错误,但是dispatch并没有注入到你的组件中。
仅当以下情况下dispatch会注入到你的组件中:
1. 未提供mapDispatchToProps
默认情况下mapDispatchToProps是简单的dispatch => ({ dispatch })。如果未提供mapDispatchToProps,则会如上所述提供dispatch。
也就是说:
// component receives `dispatch` connect(mapStateToProps /** no second argument*/)(Component)
2. 自定义的mapDispatchToProps函数返回包含dispatch的特殊对象
可以通过自定义的mapDispatchToProps函数来返回dispatch:
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement()),
reset: () => dispatch(reset()),
dispatch
}
}
或者,使用bindActionCreators:
import { bindActionCreators } from 'redux'
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ increment, decrement, reset }, dispatch)
}
}
参见:Redux的GitHub上的问题#255中action错误
关于指定mapDispatchToProps时是否向组件提供dispatch,请参考:Dan Abramov对#255的回复。
可以在Redux中使用没有mapStateToProps的mapDispatchToProps吗?
是的,第一个参数可以传入undefined或null。这样你的组件将不会订阅store,并且仍然会收到mapDispatchToProps所定义的调度props。
connect( null, mapDispatchToProps )(MyComponent)
可以调用store.dispatch吗?
直接在React组件中与store进行交互是一种反模式,无论是的显式导入store还是通过上下文进行访问(更多详细信息,请参见:Redux FAQ上的store设置)。让React Redux处理对store的访问,并使用传递给connect的dispatch来分发action。
2.2.6 相关引用及链接
教程:
- 你可能不需要
mapDispatchToProps函数
相关文档:
Q&A:
- 如何使用Redux的
connect从this.props获得简单的调度? - 如果使用
mapDispatchToProps则this.props.dispatch是undefined - 不要调用
store.dispatch,而是调用由connect注入的this.props.dispatch - 我可以在Redux没有
mapStateToProps的中使用mapDispatchToProps吗? - Redux Doc FAQ: React Redux
2.3 访问Store
React Redux提供了API使你的组件可以调度action并订阅来自store的数据更新。
作为其一部分,React Redux会抽象出你正在使用store的详细信息,以及如何处理store交互的相关信息。在典型的用法中,你自己的组件永远不需要关心这些细节,也永远不会直接引用store。React Redux还会在内部做将store和state传递到连接的组件的相关处理。
但在某些用例中,你可能需要自定义store和state传递到已连接的组件方式或直接访问store。以下是一些有关如何执行此操作的示例。
2.3.1 了解上下文的用法
在内部,React Redux使用React的“上下文”功能使Redux store可以被深度嵌套的连接组件访问。从React Redux版本6开始,这通常由React.createContext()生成的单个默认上下文对象实例(称为ReactReduxContext)处理。
React Redux的<Provider>组件使用<ReactReduxContext.Provider>将Redux store和当前store状态置于上下文中,connect使用<ReactReduxContext.Consumer>读取这些值并处理更新。
2.3.2 提供自定义的上下文
可以提供自己的自定义上下文实例,而不是使用React Redux的默认上下文实例。
<Provider context={MyContext} store={store}>
<App />
</Provider>
如果提供一个自定义上下文,React Redux将使用该上下文实例,而不是默认创建并导出的实例。
将自定义上下文提供给<Provider />之后,你需要将此上下文实例提供给所有希望连接到同一store的已连接组件:
// You can pass the context as an option to connect
export default connect(
mapState,
mapDispatch,
null,
{ context: MyContext }
)(MyComponent);
// or, call connect as normal to start
const ConnectedComponent = connect(
mapState,
mapDispatch
)(MyComponent);
// Later, pass the custom context as a prop to the connected component
<ConnectedComponent context={MyContext} />
当React Redux在其查找的上下文中找不到stoe时,将发生以下运行时错误。例如:
- 向
<Provider />提供了一个自定义上下文实例,但没有为连接的组件提供相同的实例(或未提供任何实例)。 - 为连接的组件提供了自定义上下文,但没有为
<Provider />提供相同的实例(或未提供任何实例)。
2.3.3 多个Store
Redux旨在使用单个store。但是,如果不可避免地需要使用多个store,在v6中,可以通过提供(多个)自定义上下文来做到这一点。当store位于单独的上下文实例中时,这也就相关于提供了store的自然隔离。
// a naive example
const ContextA = React.createContext();
const ContextB = React.createContext();
// assuming reducerA and reducerB are proper reducer functions
const storeA = createStore(reducerA);
const storeB = createStore(reducerB);
// supply the context instances to Provider
function App() {
return (
<Provider store={storeA} context={ContextA} />
<Provider store={storeB} context={ContextB}>
<RootModule />
</Provider>
</Provider>
);
}
// fetch the corresponding store with connected components
// you need to use the correct context
connect(mapStateA, null, null, { context: ContextA })(MyComponentA)
// You may also pass the alternate context instance directly to the connected component instead
<ConnectedMyComponentA context={ContextA} />
// it is possible to chain connect()
// in this case MyComponent will receive merged props from both stores
compose(
connect(mapStateA, null, null, { context: ContextA }),
connect(mapStateB, null, null, { context: ContextB })
)(MyComponent);
2.3.4 直接使用ReactReduxContext
在极少数情况下,可能需要直接在自己的组件中访问Redux store。这可以通过自己渲染适当的上下文使用者,并从上下文值中访问store字段来完成此操作。
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>
);
}
注意:这不被认为是React Redux公共API的一部分,并且可能会在没有通知的情况下中止使用。
2.3.5 更多资源
- CodeSandboxf示例:具有单独store的主题的阅读列表应用,通过提供(多个)自定义上下文来实现。
- 相关Issue:
2.4 静态类型
React-Redux当前使用原生JavaScript编写。但是,它可以很好的与静态类型系统(如:TypeScript和Flow)一起使用。
2.4.1 TypeScript
React-Redux没有附带自己的类型定义。如果你使用的是Typescript,则应从npm安装@types/react-redux类型定义。除了类型库函数之外,这些类型还导出一些帮助程序,以使在Redux store和React组件之间编写类型安全接口更加容易。
定义根State类型
mapState和useSelector都依赖于声明完整Redux store状态值类型。尽管可以手动编写此类型,但定义它的最简单方法是让TypeScript根据你的根reducer函数返回的内容进行推断。这样,随着reducer功能的修改,类型会自动更新。
// rootReducer.ts
export const rootReducer = combineReducers({
posts: postsReducer,
comments: commentsReducer,
users: usersReducer
})
export type RootState = ReturnType<typeof rootReducer>
// {posts: PostsState, comments: CommentsState, users: UsersState}
输入useSelector钩子
编写用于useSelector的选择器函数时,应显式定义state参数的类型。然后TS应该能够推断出选择器的返回类型,它将作为useSelector钩子的返回类型重用:
interface RootState {
isOn: boolean
}
// TS infers type: (state: RootState) => boolean
const selectIsOn = (state: RootState) => state.isOn
// TS infers `isOn` is boolean
const isOn = useSelector(selectIsOn)
要避免重复state类型声明,可以使用@types/react-redux导出的帮助程序类型来定义类型化的useSelect钩子:
// reducer.ts
import { useSelector, TypedUseSelectorHook } from 'react-redux'
interface RootState {
isOn: boolean
}
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector
// my-component.tsx
import { useTypedSelector } from './reducer.ts'
const isOn = useTypedSelector(state => state.isOn)
输入useDispatch钩子
默认情况下,useDispatch的返回值是Redux核心类型定义的标准Dispatch类型,因此不需要声明:
const dispatch = useDispatch()
如果你有Dispatch类型的自定义版本,则可以显式使用该类型:
// store.ts export type AppDispatch = typeof store.dispatch // MyComponent.tsx const dispatch: AppDispatch = useDispatch()
输入connect高级组件
手动输入connect
connect高阶组件的输入有些复杂,因为props共有3个来源:mapStateToProps、mapDispatchToProps和从父组件传入的props。以下是一个手动执行此操作的完整示例。
import { connect } from 'react-redux'
interface StateProps {
isOn: boolean
}
interface DispatchProps {
toggleOn: () => void
}
interface OwnProps {
backgroundColor: string
}
type Props = StateProps & DispatchProps & OwnProps
const mapState = (state: RootState) => ({
isOn: state.isOn
})
const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' })
}
const MyComponent = (props: Props) => (
<div style={{ backgroundColor: props.backgroundColor }}>
<button onClick={props.toggleOn}>
Toggle is {props.isOn ? 'ON' : 'OFF'}
</button>
</div>
)
// Typical usage: `connect` is called after the component is defined
export default connect<StateProps, DispatchProps, OwnProps>(
mapState,
mapDispatch
)(MyComponent)
还可以通过推断mapState和mapDispatch的类型来稍微缩短此时间:
const mapState = (state: RootState) => ({
isOn: state.isOn
})
const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' })
}
type StateProps = ReturnType<typeof mapState>
type DispatchProps = typeof mapDispatch
type Props = StateProps & DispatchProps & OwnProps
但是,如果将mapDispatch的类型定义为对象并且也引用了thunk,则会中断以此方式推断其类型。
自动推断连接的Props
connect由顺序调用的两个函数组成。第一个函数接受mapState和mapDispatch作为参数,并返回第二个函数。第二个函数接受要包装的组件,并返回一个新的包装器组件,该组件将从mapState和mapDispatch传递到props。通常两个函数可以一起调用,例如connect(mapState, mapDispatch)(MyComponent)。
从v7.1.2开始,@types/react-redux包公开了一个帮助程序类型ConnectedProps,它可以从第一个函数中提取mapStateToProp和mapDispatchToProps的返回类型。这意味着,如果将connect调用分为两步,则可以自动推断所有“来自Redux的props”,而无需手工编写。如果你已经使用React-Redux一段时间了,这种方法可能会感觉很不寻常,但是它确实大大简化了类型声明。
import { connect, ConnectedProps } from 'react-redux'
interface RootState {
isOn: boolean
}
const mapState = (state: RootState) => ({
isOn: state.isOn
})
const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' })
}
const connector = connect(
mapState,
mapDispatch
)
// The inferred type will look like:
// {isOn: boolean, toggleOn: () => void}
type PropsFromRedux = ConnectedProps<typeof connector>
然后,可以使用ConnectedProps的返回类型来输入你的props对象。
interface Props extends PropsFromRedux {
backgroundColor: string
}
const MyComponent = (props: Props) => (
<div style={{ backgroundColor: props.backgroundColor }}>
<button onClick={props.toggleOn}>
Toggle is {props.isOn ? 'ON' : 'OFF'}
</button>
</div>
)
export default connector(MyComponent)
因为可以按任何顺序定义类型,所以如果需要,仍然可以在声明连接器之前声明组件。
// alternately, declare `type Props = Props From Redux & {backgroundColor: string}`
interface Props extends PropsFromRedux {
backgroundColor: string;
}
const MyComponent = (props: Props) => /* same as above */
const connector = connect(/* same as above*/)
type PropsFromRedux = ConnectedProps<typeof connector>
export default connector(MyComponent)
推荐建议
hooks API通常更容易与静态类型一起使用。如果你正在寻找将静态类型用于React-Redux的最简单解决方案,请使用hooks API。
如果你使用的是connect,我们建议你使用ConnectedProps<T>方法从Redux推断props,因为这需要最少的显式类型声明。
2.4.2 相关资源
更多相关介绍,请参考相关资源:
- Redux 文档: TypeScript的用法: 如何声明action、reducer和store类型的示例
- Redux Toolkit 文档: 高级教程: 展示了如何在TypeScript中使用RTK和React-Redux钩子
- React+TypeScript 备忘单: 使用React与TypeScript的综合指南
- TypeScript 指南中的 React + Redux: 关于将React和Redux与TypeScript一起使用的模式的相关信息
3. API
3.1 connect()
3.1.1 概述
connect()函数用于将React组件与Redux store进行连接。
它为所连接的组件提供了store中所需的数据片段,以及可用于将action分配给store的功能。
它不会修改传递它的组件类。而是返回一个新的,对传入组件包装后的、已连接的组件类。
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
mapStateToProps和mapDispatchToProps分别处理Redux store的state和dispatch。state和dispatch会作为第一个参数提供给mapStateToProps和mapDispatchToProps函数。
mapStateToProps和mapDispatchToProps的返回值别称为stateProps和dispatchProps。它们将作为第一和第二个参数提供给mergeProps(如果已定义),其中第三个参数是ownProps。合并后的结果(通常称为mergeProps)将提供给所连接的组件。
3.1.2 connect()参数
connect接受4个参数,都是可选参数。依照惯例,它们被称为:
mapStateToProps?: FunctionmapDispatchToProps?: Function | ObjectmergeProps?: Functionoptions?: Object
mapStateToProps?: (state, ownProps?) => Object
如果指定了mapStateToProps函数,则新的包装器组件将订阅Redux store更新。这意味着每当store更新时,都会调用mapStateToProps。mapStateToProps的结果必须是一个普通对象,该对象将合并到包装组件的props中。如果不想订阅store更新,请传入null或undefined代替mapStateToProps参数。
参数
mapStateToProps函数可接受以上两个参数。声明的函数参数(也称arity)的数量会影响何时调用它。这也确定该函数是否将接收ownProps。请参阅此注释。
state
如果mapStateToProps函数声明为使用一个参数,则只要store的state发生更改,就会调用该函数,并且将store的state作为唯一参数
const mapStateToProps = state => ({ todos: state.todos })
ownProps
如果mapStateToProps函数声明为使用两个参数,那么store的state发生更改或包装组件收到新的props(基于浅比较),就会调用该函数。
依照惯例,第二个参数通常称为ownProps。
const mapStateToProps = (state, ownProps) => ({
todo: state.todos[ownProps.id]
})
返回值
mapStateToProps函数应返回一个对象。通常称为stateProps,该对象会做为props合并到所连接的组件。如果定义了mergeProps,它将作为mergeProps的第一个参数提供。
mapStateToProps的返回值确定所连接的组件是否将重新渲染(详细信息)。
更多关于mapStateToProps的使用,请参阅:mapStateToProps使用指南。
mapDispatchToProps?: Object | (dispatch, ownProps?) => Object
connect()的第二个参数通常称为mapDispatchToProps,其可以是对象、函数或不提供。
默认情况下,即不提供connect()的第二个参数,组件将收到dispatch信息:
// do not pass `mapDispatchToProps` connect()(MyComponent) connect(mapState)(MyComponent) connect( mapState, null, mergeProps, options )(MyComponent)
如果mapDispatchToProps做为函数提供,其最多可有以下两个参数:
参数
dispatch: Function
如果mapDispatchToProps声明为带一个参数的函数,则它将被赋予store的dispatch。
const mapDispatchToProps = dispatch => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' })
}
}
ownProps?: Object
如果mapDispatchToProps函数声明为带两个参数,则将使用dispatch作为第一个参数调用该属性,并将传递给包装器组件的props作为第二个参数,且只要所连接的组件收到新的props就会重新调用它。
依照惯例,第二个参数通常称为ownProps。
// binds on component re-rendering
;<button onClick={() => this.props.toggleTodo(this.props.todoId)} />
// binds on `props` change
const mapDispatchToProps = (dispatch, ownProps) => {
toggleTodo: () => dispatch(toggleTodo(ownProps.todoId))
}
返回值
mapDispatchToProps函数应返回一个对象。对象的每个字段都应该是一个函数,调用该函数会将对应的action分发(dispatch)给store。
mapDispatchToProps的返回值被称为dispatchProps。其将作为props合并到连接的组件。如果定义了mergeProps,它将作为mergeProps的第二个参数提供。
const createMyAction = () => ({ type: 'MY_ACTION' })
const mapDispatchToProps = (dispatch, ownProps) => {
const boundActions = bindActionCreators({ createMyAction }, dispatch)
return {
dispatchPlainObject: () => dispatch({ type: 'MY_ACTION' }),
dispatchActionCreatedByActionCreator: () => dispatch(createMyAction()),
...boundActions,
// you may return dispatch here
dispatch
}
}
更多相关介绍,请参阅:mapDispatchToProps使用指南
对象简写形式
mapDispatchToProps可以是每个字段都是action cretor的对象。
import { addTodo, deleteTodo, toggleTodo } from './actionCreators'
const mapDispatchToProps = {
addTodo,
deleteTodo,
toggleTodo
}
export default connect(
null,
mapDispatchToProps
)(TodoApp)
在这种情况下,React-Redux使用bindActionCreators将的dispatch绑定的每个action creator。结果将被作为dispatchProps,其将直接合并到所连接的组件,或者作为第二个参数提供给mergeProps。
mergeProps?: (stateProps, dispatchProps, ownProps) => Object
如果指定,则定义如何确定自己包装的组件的最终props。如果不提供mergeProps,则包装的组件默认会收到{ ...ownProps, ...stateProps, ...dispatchProps }
参数
mergeProps最多可指定以下三个参数。它们分别是mapStateToProps()、mapDispatchToProps()和包装器组件的props的结果:
statePropsdispatchPropsownProps
其返回的普通对象中的字段将用作包装组件的props。可以指定此函数以根据props选择state的一部分,或将action creator绑定到props中的特定变量。
返回值
mergeProps的返回值称为mergeProps,并且这些字段将用作包装组件的props。
options?: Object
{
context?: Object,
pure?: boolean,
areStatesEqual?: Function,
areOwnPropsEqual?: Function,
areStatePropsEqual?: Function,
areMergedPropsEqual?: Function,
forwardRef?: boolean,
}
context?: Object
需要 >= v6.0 的版本
React-Redux v6允许你提供一个供React-Redux使用的自定义上下文实例。即,将上下文的实例传递<Provider />和所连接的组件。可以通过将上下文作为选项字段以传递给所连接的组件,或将其作为渲染中对所连接组件的支持。
// const MyContext = React.createContext();
connect(
mapStateToProps,
mapDispatchToProps,
null,
{ context: MyContext }
)(MyComponent)
pure?: boolean
- 默认值:
true
假设包装的组件是“纯组件”,除了其props和所选Redux store的state外,不依赖任何输入或state。
options.pure为true时,connect会执行一些相等性检查,以避免不必要的调用mapStateToProps、mapDispatchToProps、mergeProps而导致的渲染。这些检查包括:areStatesEqual、areOwnPropsEqual、areStatePropsEqual和areMergedPropsEqual。
areStatesEqual?: (next: Object, prev: Object) => boolean
- 默认值:
strictEqual: (next, prev) => prev === next
纯组件时(pure=true),将传入store的state与其之前的值进行比较。
示例1:
const areStatesEqual = (next, prev) => prev.entities.todos === next.entities.todos
如果你的mapStateToProps函数计算上开销较大,并且仅与state的一小部分有关,则你可能希望覆盖areStatesEqual。上面的示例将有效忽略state片段以外的所有内容的state修改。
示例2:
如果是非纯reducer会改变store的state,则可能希望覆盖areStatesEqual以便始终返回false
const areStatesEqual = () => false
这也可能会影响其他相等性检查,具体取决于mapStateToProps函数。
areOwnPropsEqual?: (next: Object, prev: Object) => boolean
- 默认值:
shallowEqual: (objA, objB) => boolean(对象的每个字段都相等则返回true)
纯组件时(pure=true),将传入props与其之前的值进行比较。
你可能希望重写areOwnPropsEqual作为将传入props列入白名单的一种方法。这时还必须实现mapStateToProps、mapDispatchToProps和mergeProps。
areStatePropsEqual?: (next: Object, prev: Object) => booleant
- 类型:
function - 默认值:
shallowEqual
纯组件时(pure=true),会将mapStateToProps的返回结果与其之前的值进行比较。
areMergedPropsEqual?: (next: Object, prev: Object) => boolean
- 默认值:
shallowEqual
纯组件时(pure=true),会将mergeProps的返回结果与其之前的值进行比较。
如果你的mapStateToProps使用一个记忆化的选择器,该选择器仅在相关属性发生更改时才返回一个新对象,所以你可能希望覆盖areStatePropsEqual以使用strictEqual。这是非常微小的性能改进,因为这会避免每次调用mapStateToProps时对单个props进行额外的相等性检查。
如果选择器产生复杂的props,那么可能希望重写areMergedPropsEqual以实现deepEqual。例如:嵌套的对象、新的数组等。(深层相等检查可能比重新渲染要快。)
forwardRef?: boolean
需要 >= v6.0 的版本
如果{forwardRef : true}传给connect,向连接的包装器组件添加<ref实际上将返回被包装的组件的实例。
3.1.3 connect()返回值
connect()的返回值是一个包装函数,该函数接收你的组件,并返回一个包装器组件及其注入的其他prop。
import { login, logout } from './actionCreators'
const mapState = state => state.user
const mapDispatch = { login, logout }
// first call: returns a hoc that you can use to wrap any component
const connectUser = connect(
mapState,
mapDispatch
)
// second call: returns the wrapper component with mergedProps
// you may use the hoc to enable different components to get the same behavior
const ConnectedUserLogin = connectUser(Login)
const ConnectedUserProfile = connectUser(Profile)
在大多数情况下,包装函数将立即调用,而不是保存在临时变量中:
import { login, logout } from './actionCreators'
const mapState = state => state.user
const mapDispatch = { login, logout }
// call connect to generate the wrapper function, and immediately call
// the wrapper function to generate the final wrapper component.
export default connect(
mapState,
mapDispatch
)(Login)
3.1.4 使用示例
connect的使用非常灵活,以下是的一些调用示例:
- 仅注入
dispatch,而不监听store:
export default connect()(TodoApp)
- 注入所有action creator(
addTodo,completeTodo, ...),而无需订阅store:
import * as actionCreators from './actionCreators' export default connect( null, actionCreators )(TodoApp)
- 注入
dispatch和全局state中的每个字段:
// don't do this! export default connect(state => state)(TodoApp)
实现应该中不推荐这样做
- 注入
dispatch和todos:
function mapStateToProps(state) {
return { todos: state.todos }
}
export default connect(mapStateToProps)(TodoApp)
- 注入
todos和所有action creator:
import * as actionCreators from './actionCreators'
function mapStateToProps(state) {
return { todos: state.todos }
}
export default connect(
mapStateToProps,
actionCreators
)(TodoApp)
- 注入
todos和所有action creator(addTodo,completeTodo, ...)做为action:
import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) }
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)
- 注入
todos和指定的action creator(addTodo):
import { addTodo } from './actionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ addTodo }, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)
- 使用简写形式注入
todos和指定的action creator(addTodo和deleteTodo):
import { addTodo, deleteTodo } from './actionCreators'
function mapStateToProps(state) {
return { todos: state.todos }
}
const mapDispatchToProps = {
addTodo,
deleteTodo
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)
- 注入
todos、todoActionCreators作为todoActions,和counterActionCreators作为counterActions:
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return {
todoActions: bindActionCreators(todoActionCreators, dispatch),
counterActions: bindActionCreators(counterActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)
- 注入
todos、todoActionCreators和counterActionCreators共同作为actions:
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(
{ ...todoActionCreators, ...counterActionCreators },
dispatch
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)
- 注入
todos、todoActionCreators和counterActionCreators直接作为props:
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ ...todoActionCreators, ...counterActionCreators },
dispatch
)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)
- 根据props注入指定用户的
todos:
import * as actionCreators from './actionCreators'
function mapStateToProps(state, ownProps) {
return { todos: state.todos[ownProps.userId] }
}
export default connect(mapStateToProps)(TodoApp)
- 根据props注入指定用户的
todos,并将props.userId注意action:
import * as actionCreators from './actionCreators'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mergeProps(stateProps, dispatchProps, ownProps) {
return Object.assign({}, ownProps, {
todos: stateProps.todos[ownProps.userId],
addTodo: text => dispatchProps.addTodo(ownProps.userId, text)
})
}
export default connect(
mapStateToProps,
actionCreators,
mergeProps
)(TodoApp)
3.1.5 注意
mapToProps函数的Arity
mapStateToProps和mapDispatchToProps的已声明函数参数的数量决定它们是否收到ownProps
注意:如果函数的定义中包含一个强制性参数(函数的长度为1),则ownProps不会传递给mapStateToProps和mapDispatchToProps。例如,以下定义的函数将不会收到ownProps作为第二个参数
function mapStateToProps(state) {
console.log(state) // state
console.log(arguments[1]) // undefined
}
const mapStateToProps = (state, ownProps = {}) => {
console.log(state) // state
console.log(ownProps) // {}
}
没有强制参数或两个参数 * 的函数将收到ownProps。
const mapStateToProps = (state, ownProps) => {
console.log(state) // state
console.log(ownProps) // ownProps
}
function mapStateToProps() {
console.log(arguments[0]) // state
console.log(arguments[1]) // ownProps
}
const mapStateToProps = (...args) => {
console.log(args[0]) // state
console.log(args[1]) // ownProps
}
工厂函数
如果mapStateToProps和mapDispatchToProps函数返回一个函数,则在组件实例化时将调用它们一次,并且它们的返回值将在随后的调用中分别用作实际的mapStateToProps、mapDispatchToProps函数。
工厂功能通常与memoized selector一起使用。这使你能够在闭包内部创建特定于组件实例的选择器:
const makeUniqueSelectorInstance = () =>
createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
)
const makeMapState = state => {
const selectItemForThisComponent = makeUniqueSelectorInstance()
return function realMapState(state, ownProps) {
const item = selectItemForThisComponent(state, ownProps.itemId)
return { item }
}
}
export default connect(makeMapState)(SomeComponent)
3.2 Provider
3.2.1 概述
<Provider />使Redux的store可用于connect()函数中包装的所有嵌套组件。
由于可以连接React Redux应用中的任何React组件,因此大多数应用中都会在顶层渲染code><Provider />,并在其中包含整个应用的组件树。
通常来说,除非嵌套在<Provider />内,否则不能使用所连接的组件。
Props
store(Redux Store)应用中的单个Redux store。
children(ReactElement)组件层次结构的根。
content你可以提供一个上下文实例。如果这样,则还需要为所有连接的组件提供相同的上下文实例。不正确的上下文会导致运行时错误。
注意:无需提供自定义上下文即可访问store。React Redux会默认导出其使用的上下文实例,以便可以通过以下方式访问store:
import { ReactReduxContext } from 'react-redux'
// in your connected component
render() {
return (
<ReactReduxContext.Consumer>
{({ store }) => {
// do something with the store here
}}
</ReactReduxContext.Consumer>
)
}
使用示例
下面的示例中,<App />是我们的根组件,也就是说它位于我们组件层次结构的最顶层。
普通React示例:
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { App } from './App'
import createStore from './createReduxStore'
const store = createStore()
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
和React Router一起使用:
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { Router, Route } from 'react-router-dom'
import { App } from './App'
import { Foo } from './Foo'
import { Bar } from './Bar'
import createStore from './createReduxStore'
const store = createStore()
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route exact path="/" component={App} />
<Route path="/foo" component={Foo} />
<Route path="/bar" component={Bar} />
</Router>
</Provider>,
document.getElementById('root')
)
3.3 connectAdvanced()
connectAdvanced(selectorFactory, connectOptions?)
将React组件连接到Redux Store。它是connect()的基础,但并不关心如何将state、props和dispatch组合为最终的props。
其不会对默认值或结果记录做任何假设,而将这些责任留给了调用者。也不会修改传给它的组件类,而是返回一个新的、已连接的组件类。
大多数应用并不这个函数,因为connect()已适用于大多数用例。
注意:connectAdvanced是在5.0版中添加的,并且connect重新实现为connectAdvanced的一组特定参数。
3.3.1 参数
selectorFactory(dispatch, factoryOptions): selector(state, ownProps): props(Function): 初始化选择器函数(在每个实例的构造函数期间)。由于store的state更改或接收新props,连接器组件需要计算新props时,都会调用该选择器函数。selector的计算结果应为纯对象,该对象作为props传递给包装的组件。如果对selector的连续调用返回与其先前调用返回的对象相同(===),则不会重新渲染该组件。selector有责任在适当的时候返回前一个对象。[
connectOptions] (Object) 如果指定,则进一步自定义连接器的行为。[
getDisplayName] (Function): 计算相对于包装组件的连接器组件的displayName属性。通常由包装函数覆盖。默认值:name => 'ConnectAdvanced('+name+')'[
methodName] (String): 显示在错误消息中。通常由包装函数覆盖。默认值:'connectAdvanced'[
renderCountProp] (String): 如果指定,则名为此值的属性将添加到传递给包装组件的props中。其值将是渲染组件的次数,这对于跟踪不必要的重新渲染很有用。默认值:undefined[
shouldHandleStateChanges] (Boolean): 控制连接器组件是否订阅Redux Storestate状态更改。如果设置为false,则仅在重新渲染父组件时才会重新渲染默认值:true[
forwardRef] (Boolean): 如果为true,则向所连接的包装器组件添加一个ref,实际将返回被包装的组件的实例此外,通过
connectOptions传递的所有其他选项都会在factoryOptions参数中传递给selectorFactory。
3.3.2 返回值
一个更高阶的React组件类,该类从store的state构建props并将它们传给所包装的组件。高阶组件是一个接受组件参数并返回新组件的函数。
静态属性
WrappedComponent(Component): 传给connectAdvanced(...)(Component)的原始组件类
静态方法
组件的所有原始静态方法都会被提取
3.3.3 备注
由于
connectAdvanced会返回一个高阶组件,因此需要调用两次。第一次使用上述参数,第二次使用以下组件:connectAdvanced(selectorFactory)(MyComponent).connectAdvanced不会修改传入的React组件,而是返回一个新的、已连接的组件
示例
根据props注入特定用户的todos,并将props.userId注入action中:
import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'
function selectorFactory(dispatch) {
let ownProps = {}
let result = {}
const actions = bindActionCreators(actionCreators, dispatch)
const addTodo = text => actions.addTodo(ownProps.userId, text)
return (nextState, nextOwnProps) => {
const todos = nextState.todos[nextOwnProps.userId]
const nextResult = { ...nextOwnProps, todos, addTodo }
ownProps = nextOwnProps
if (!shallowEqual(result, nextResult)) result = nextResult
return result
}
}
export default connectAdvanced(selectorFactory)(TodoApp)
3.4 batch()
batch(fn: Function)
React的unstable_batchedUpdate()API允许将事件循环中的任何React更新一起批处理到单个渲染过程中。React已经在内部将其用于自己的事件处理程序回调。该API实际上是像ReactDOM或React Native之类的渲染器包的一部分,而不是React核心本身。
由于React-Redux需要在ReactDOM和React Native环境中工作,因此其已经在构建时从正确的渲染器导入合适的API使用。我们还可以自己导出此函数,并将其重命名为batch(),可以使用它来确保在React之外分发(dispatch)的多个action仅渲染一次,如下所示:
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())
})
}
}
3.5 钩子
React的“钩子”(hooks)API使函数组件能够使用本地组件state、执行边际等。
React Redux中已提供了一组钩子API,以替代现有的connect()高阶组件。这些API允许你订阅Redux store和调度action,而无需将组件包装在connect()中。
钩子相关API添加于v7.1.0
3.5.1 在React Redux App中使用钩子
与connect()一样,首先应该将整个应用程序包装在<Provider />组件中,以使store在整个组件树中可用:
const store = createStore(rootReducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
这一步中,你可以导入任何的React Redux钩子API并在函数组件中使用它们。
3.5.2 useSelector()
const result : any = useSelector(selector : Function, equalityFn? : Function)
使你可使用选择器功能从Redux store的state中提取数据。
注意:选择器函数应该是纯函数,因为它可能在任意时间点多次执行。
从概念上讲,选择器大致等效于connect的mapStateToProps参数。其会使用整个Redux store的state作为唯一参数来调用选择器。只要函数组件渲染,选择器就会运行。useSelector()还会订阅Redux store,并在分发action时运行选择器。
传递给useSelector()的选择器和mapState函数之间存在一些差异:
- 选择器可能返回任何结果,而不仅是对象。选择器的返回值将用作
useSelector()钩子的返回值。 - 调度动作(dispatch action)时,
useSelector()将对之前选择器结果值和当前结果值进行浅比较。如果不同,则强制重新渲染组件;如果相同,则组件不会重新渲染。 - 选择器函数未收到
ownProps参数。但是,可以通过闭包(参见下面示例)或通过当前选择器来使用props。 - 使用记忆选择器时必须格外小心(更多详细信息,参见下方示例)。
useSelector()默认使用严格相等(===)进行检查,而非浅比较(更多详细信息,参见下方示例)。
相等性比较和更新
当函数组件渲染时,将调用传入的选择器函数,并且其结果将从useSelector()钩子返回。(如果选择器已运行且未发生更改,则可能返回缓存的结果。)
但是,当将action分发到Reduxstore时,仅当选择器结果与上一个结果不同时,useSelector()才强制重新渲染。从v7.1.0-alpha.5开始,默认使用严格相等(===)进行比较。这与connect()不同,connect()对mapState调用的结果使用是浅相等来确定是否需要重新渲染。这对如何使用useSelector()有一些影响。
使用mapState时,所有独立字段都会在组合的对象中返回。返回对象是否是重新引用都没有关系-connect()只是比较了各个字段。使用useSelector()时,每次返回一个新对象都会默认强制重新渲染。如果要从store中查找多个值,则可以:
- 多次调用Call
useSelector(),每次都返回一个单字段值 - 使用“Reselect”或类似的库来创建一个记忆选择器,该选择器在一个对象中返回多个值,但是仅当其中一个值已更改时才返回新对象。
- 使用React-Redux中的
shallowEqual函数作为useSelector()的equityFn参数,例如: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 selector)
如上所示,将useSelector与内联选择器一起使用时,无论何时渲染组件,都会创建新的选择器实例。只要选择器不保持任何state,此方法就起作用。但是,记忆化选择器(例如,通过reselect的createSelector创建的选择器)确实有内部状态,因此在使用它们时必须格外小心。
当选择器仅依赖state时,只需确保将其声明在组件外部,以便为每个渲染使用相同的选择器实例:
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,结果也是true,但只会在单个组件的单个实例中使用:
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时,需要确保每个组件实例都有其自己的选择器实例(请参阅:这里,以了解更多详细信息):
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const makeNumOfTodosWithIsDoneSelector = () =gt;
createSelector(
state =gt; state.todos,
(_, isDone) =gt; isDone,
(todos, isDone) =gt; todos.filter(todo =gt; todo.isDone === isDone).length
)
export const TodoCounterForIsDoneValue = ({ isDone }) =gt; {
const selectNumOfTodosWithIsDone = useMemo(
makeNumOfTodosWithIsDoneSelector,
[]
)
const numOfTodosWithIsDoneValue = useSelector(state =gt;
selectNumOfTodosWithIsDoneValue(state, isDone)
)
return <divgt;{numOfTodosWithIsDoneValue}</divgt;
}
export const App = () =gt; {
return (
<gt;
<spangt;Number of done todos:</spangt;
<TodoCounterForIsDoneValue isDone={true} /gt;
<spangt;Number of unfinished todos:</spangt;
<TodoCounterForIsDoneValue isDone={false} /gt;
</gt;
)
}
3.5.3 useDispatch()
const dispatch = useDispatch()
这个钩子从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对其进行保存,否则,由于更改了引用,子组件可能会不必要地渲染。
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>
))
3.5.4 useStore()
const store = useStore()
该钩子返回对传递给<Provider>组件的同一Redux store的引用。
这一钩子不应经常使用,而主要选择使用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>
}
3.5.5 使用注意
过时props与“僵尸子节点”
React Redux 实现中最困难的方面之一是,如果将mapStateToProps函数定义为(state, ownProps),则每次都会使用“最新”的props来调用它。直到v4为止,抛出的重复错误都涉及到极端情况,例如:从mapState函数抛出的针对刚删除数据的列表项的错误。
从v5开始,React Redux已尝试保证与ownProps的一致性。在v7中,这是在connect()内部使用自定义Subscription类实现的,该类形成了嵌套的层次结构。这样可以确保树中较低的已连接组件仅在最近的已连接上级组件更新后才接收store更新通知。但是,这依赖于覆盖内部React上下文的每个connect()实例,提供其自己唯一的Subscription实例以形成该嵌套,并使用新的上下文值渲染<ReactReduxContext.Provider>。
使用钩子,无法渲染上下文提供程序,这意味着也没有嵌套的监听层次结构。因此,在依赖于使用钩子而不是connect()的应用程序中,过时props和“僵尸子节点”问题可能会再次发生。
具体来说,“过时props”是指以下情况:
- 选择器函数依赖于此组件的prop来提取数据
- action会导致父组件重新渲染并传递新props”
- 但是该组件的选择器函数在该组件有机会用这些新props重新渲染之前执行
根据使用了哪些props以及当前store的state,这可能导致选择器返回错误数据,甚至引发错误。
“僵尸子节点”指以下情况:
- 多个嵌套连接的组件在第一次安装,导致子组件在其父项之前订阅store
- 然后分发一个从store删除数据的action
- 这时父组件将停止渲染该子组件
- 但是,由于子项首先订阅,因此其订阅项在父项停止渲染前运行。当它基于prop从store中读取一个值时,该数据将不再存在,并且如果提取逻辑不当,则可能导致引发错误。
useSelector()试图通过捕获由于store更新而在执行选择器时引发的所有错误(但在渲染期间执行该选择器时不捕获)来捕获所有错误。发生错误时,将强制渲染组件,此时将再次执行选择器。只要选择器是纯函数,并且不依赖选择器抛出错误,此方法就会起作用。
如果希望尝试手动解决此问题,请使用useSelector()来避免这些问题,以下是一些可能的选择:
- 不要依赖选择器函数中的props来提取数据
- 如果确实依赖选择器函数中的props,而这些props可能会随时间变化,或者提取的数据可能基于可以删除的项目,请尝试防御性地编写选择器函数。不要直接读取
state.todos[props.id].name,而是先读取state.todos[props.id],并在读取todo.name前进行验证。 - 因为
connect会将必要的Subscription添加到上下文提供者,并且延迟了对子订阅的判断直到所连接的组件重新渲染,所以只要使用useSelector将已连接的组件放在组件树中的组件上方,就可以防止这些问题。由于与hooks组件相同的store更新,因此重新渲染了连接的组件。
性能
如前所述,默认情况下,在分发action后运行选择器函数时,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)
