Connect:使用mapStateToProps抽取数据
作为传递给connect的第一个参数,mapStateToProps用来从store中选择被连接的组件所需要的部分数据。常以mapState缩写来表示。
-
每当store state改变时就被调用
-
接收整个store state,并且返回一个组件需要的数据对象
声明mapStateToProps
mapStateToProps应该声明为一个方法:
function mapStateToProps(state, ownProps?)
他接收的第一个参数是state,可选的第二个参数时ownProps,然后返回一个被连接组件所需要的数据的纯对象。
这个方法应作为第一个参数传递给connect,并且会在每次Redux store state改变时被调用。如果你不希望订阅store,那么请传递null或者undefined替代mapStateToProps作为connect的第一个参数。
无论**mapStateToProps是使用function关键字声明的(function mapState(state) { } )** 还是以一个箭头函数**(const mapState = (state) => { } )** 的形式定义的——它都能够以同样的方式生效。
参数
-
state -
ownProps(可选)
state
mapStateToProps的第一个参数是整个Redux store state对象(与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****(可选)
如果你的组件需要用自身的props数据以从store中检索出数据,你可以传入第二个参数,ownProps。这个参数将包含所有传递给由connect生成的包装组件的props。
// Todo.js
function mapStateToProps(state, ownProps) {
const { visibilityFilter } = state;
const { id } = ownProps;
const todo = getTodoById(state, id);
// 组件额外接收:
return { todo, visibilityFilter };
};
// 之后,在你的应用里,渲染一个如下父组件:
<ConnectedTodo id={123} />
// 你的组件接收 props.id, props.todo, 以及 props.visibilityFilter
你不需要把ownProps中的值再添加入mapStateToProps返回的对象中,connect会自动的把这些不同源的prop合并为一个最终的prop集。
返回值
你的mapStateToProps方法应该返回一个包含了组件用到的数据的纯对象:
-
每一个对象中的字段都将作为你组件的一个
prop -
字段中的值用来判断你的组件是否需要重新渲染
例如:
function mapStateToProps(state) {
return {
a : 42,
todos : state.todos,
filter : state.visibilityFilter
}
}
// 组件会接收: props.a, props.todos,以及 props.filter
注意:在一些高级场景中,你可能需要更多地对于渲染性能的控制,
mapStateToProps也可以返回一个方法。在这种情况下,那个所返回的方法会做为一个特定组件实例的最终的mapStateToProps。这样一来,你就可以对每个实例进行memoization。参考高级用法部分以获取更多信息。也可以看PR #279以及上面增加的测试。但大部分应用根本不需要这样做
使用指南
让mapStateToProps改造从store中取出的数据
mapStateToProps方法能够,且应该,做更多的事情,而不仅仅是返回一个state.someSlice。他们有责任去改造组建所需要的store中的数据。比如说,返回一个特定prop名称的值,从state树中不同部分取出数据片段并合并为一个整体,以及以不同的方式转化store。
使用Selector方法去抽取和转化数据
我们强烈建议使用selector方法去封装抽取state树中的特定位置的值。Memoized selector方法也在提高应用性能上起到了关键作用。(参考本页以下部分:高级用法:性能以获取更多关于为何以及如何使用selectors的细节)
mapStateToProps方法应该足够快
一旦store改变了,所有被连接组件中的所有的mapStateToProps方法都会运行。因此,你的mapStateToProps方法一定要足够快。这也意味着缓慢的mapStateToProps方法会成为你应用的一个潜在瓶颈。
作为“重塑数据”想法的一部分,mapStateToProps方法经常需要以各种方式来转化数据(比如过滤数组,遍历IDs数组映射到对应的对象,或者从Immutable.js对象中抽取纯js值)。这些转化的开销通常很高昂,不仅表现在运行转化操作的开销上,也表现在判断最终是否要更新组件上。如果的确需要考虑性能问题了,那么要确保你的这些转化只发生在所输入的值发生变化的时候。
mapStateToProps方法应该纯净且同步
正如Redux Reducer,一个mapStateToProps方法应该是100%纯净的并且是同步的。他应该仅接受state(以及ownProps)作为参数,然后以props形式返回组件所需要的数据。他不应该触发任何异步操作,如AJAX请求数据,方法也不能声明为async形式。
mapStateToProps和性能
返回值决定你的组件是否需要更新
React-Redux 内部实现了shouldComponentUpdate方法以便在组件用到的数据发生变化后能够精确地重新渲染。默认地,React-Redux使用“===”对mapStateToProps返回的对象的每一个字段逐一对比,以判断内容是否发生了改变。但凡有一个字段改变了,你的组件就会被重新渲染以便接收更新过的props值。注意到,返回一个相同引用的突变对象(mutated object)是一个常见错误,因为这会导致你的组件不能如期重新渲染。
总结一下传入mapStateToProps参数来抽取store中的数据的connect方法包装过的组件行为:
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.assgin -
使用扩展运算符:
{...oldState,...newData}
把这些操作放在memeoized selector functions中确保它们只在输入值变化后运行。这样也能够确保如果输入值没有改变,mapStateToProps仍然返回与之前的相同值,然后connect就能够跳过重渲染过程。
仅在数据改变时运行开销大的操作
转化数据经常开销很大(并且通常会创建一个新的对象引用)。为了使你的mapStateToProps方法足够快,你应该仅在相关数据改变时重新运行这些复杂的转化。
有下面几种形式来达到这样的目的:
-
一些转化可以在action创建函数或者reducer中运算,然后可以把转化过的数据储存在store中
-
转换也可以在组件的
render()生命周期中完成 -
如果转化必须要在
mapStateToProps方法中完成,那么我们建议使用memoized selector functions以确保转化仅发生在输入的数据变化时。
考虑Immutable.js性能
Immutable.js 的作者Lee Byron在Twitter中明确建议了如果开始考虑性能了要避免使用toJS
Perf tip for #immutablejs: avoid .toJS() .toObject() and .toArray() all slow full-copy operations which render structural sharing useless.
还有一些别的关于Immutable.js的性能提升建议——参见下方的链接列表。
行为及总结
mapStateToProps在store state相同的情况下不会运行
connect生成的包装组件会订阅Redux store。每当action被分发后,它就调用store.getState()并检查是否lastState===currentState。如果两个状态值引用完全相同,那么mapStateToProps就不会运行,因为组件假设了你余下的store state也没有发生改变。
Redux的combineReducers功能函数会尝试对其优化。如果所有reducer都没有返回新值,那么combineReducers会返回旧state对象而不是新的。这意味着,一个reducer中的突变不会使根state对象更新,当然UI也不会重新渲染。
声明参数的数量影响行为
当仅有(state)时,每当根store state对象不同了,函数就会运行。
当有(state,ownProps)两个参数时,每当store state不同、或每当包装props变化时,函数都会运行。
这意味着**你不应该增加****ownProps**参数,除非你实在是需要它,否则你的mapStateToProps函数会比它实际需要运行次数运行更多次。
关于这个行为有一些极端案例。arguments的数量决定了**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
}
如果之前定义的函数包含了0个或2个命令参数,他就需要接收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
}
链接和参考
教程
-
Practical Redux Series, Part 6: Connected Lists, Forms, and Performance
-
Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance
性能
Q&A
-
Why isn't my component re-rendering, or my mapStateToProps running
-
Should I only connect my top component, or can I connect multiple components in my tree?
Connect: 使用mapDispatchToProps分发actions
作为第二个传入connect的参数,mapDispatchToProps可以实现向store中分发acions。
dispatch是Redux store实例的一个方法,你会通过store.dispatch来分发一个action。这也是唯一触发一个state变化的途径。
使用React-Redux后,你的组件就不再需要直接和store打交道了——connect会为你完成这件任务,React-Redux提供了两种可以分发actions的方式:
-
默认地,一个已连接组件可以接收
props.dispatch然后自己分发actions -
connect能够接收一个mapDispatchToProps作为第二个参数,这将让你能够创建dispatch调用方法,然后把这些方法作为props传递给你的组件。
分发(Dispatching)的途径
默认:作为一个prop的dispatch
如果你不为connect()指明第二个参数,你的组件会默认接收dispatch。比如:
connect()(MyComponent);
// 与下面语句等价
connect(
null,
null
)(MyComponent);
// 或者
connect(mapStateToProps /** 没有第二个参数 */)(MyComponent);
一旦你以这种方式连接了你的组件,你的组件就会接收props.dispatch。你可以用它来向store中分发actions。
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参数能够让你指明你的组件所实际需要分发的那些actions。它允许你提供action分发函数作为props,这样一来,你不用再进行props.dispatch(() => increment())调用,你可以直接props.increment()。你这么做是出于下面几个原因:
更加声明式的
首先,把分发逻辑封装到函数中使得整个实现更加声明式。分发一个action然后让Redux store处理数据流,表现出了你如何实现这一行为而不仅仅是只关心它做了什么。
单击按钮后分发一个action也许是个不错的例子。把一个button直接连接从概念上来讲有点说不通,button也没有dispatch引用。
// button需要有意识地 "dispatch"
<button onClick={() => dispatch({ type: "SOMETHING" })} />
// button看起来不像在 "dispatch",
<button onClick={doSomething} />
一旦你把所有的action creators使用函数封装起来之后,你的组件就不需要再dispatch了。因此,如果你定义了**mapDispatchToProps被连接组件就不再接收到dispatch**了。
把action分发逻辑向子(未连接)组件传递
此外,你现在也能够向下传递你的action分发函数给子组件(可能尚未连接)。这样就能够使更多的组件分发actions,且它们对Redux也是无感知的。
// 把toggleTodo 传递给子组件
// 让Todo 能分发 toggleTodo action
const TodoList = ({ todos, toggleTodo }) => (
<div>
{todos.map(todo => (
<Todo todo={todo} onClick={toggleTodo} />
))}
</div>
);
这就是React-Redux的connect所做的工作——封装与Redux Store对话的逻辑,并且你不再需要操心。你也应该在你的实现中充分利用这一点。
两种mapDispatchToProps的形式
mapDispatchToProps参数有两种形式:函数形式自定义化程度更高,对象形式更简单。
-
函数形式:更高自由度、能够访问
dispatch和可选择的ownProps -
对象形式:更声明式,更易于使用
注意:我们建议使用对象形式的
mapDispatchToProps,除非你需要以某种自定义形式进行分发操作
将mapDispatchToProps定义为一个函数
将mapDispatchToProps定义为一个函数使你更灵活地定义你的组件能够接收到的函数、以及这些函数如何分发actions。你对dispatch和ownProps都具有访问权限。你可以借此机会编写你的连接组件的自定义函数。
参数
-
dispatch -
ownProps(可选)
dispatch
mapDispatchToProps函数调用时以dispatch作为第一个参数。通常情况下,你会利用这个参数来返回一个内部调用了dispatch()的新函数,然后内部传递一个纯的action对象或者action创建函数的返回值。
const mapDispatchToProps = dispatch => {
return {
// 分发纯action对象
increment: () => dispatch({ type: "INCREMENT" }),
decrement: () => dispatch({ type: "DECREMENT" }),
reset: () => dispatch({ type: "RESET" })
};
};
你也可能需要把一些参数转发给你的action创建函数
const mapDispatchToProps = dispatch => {
return {
// 直接转发参数
onClick: event => dispatch(trackClick(event)),
// 间接转发参数
onReceiveImpressions: (...impressions) =>
dispatch(trackImpressions(impressions))
};
};
ownProps****(可选)
你的mapDispatchToProps函数是可以接收两个参数的,第一个是dispatch,传递给连接组件的props即为mapDispatchToProps的第二个参数,然后在组件接收到新的props后会重新调用。
这意味着,你应该在组件props改变阶段重新把新的props绑定到action分发函数中去,而不是在组件重新渲染阶段进行。
// 在组件re-rendering阶段绑定
<button onClick={() => this.props.toggleTodo(this.props.todoId)} />;
// 在 props 改变阶段绑定
const mapDispatchToProps = (dispatch, ownProps) => {
toggleTodo: () => dispatch(toggleTodo(ownProps.todoId));
};
返回值
你的mapDispatchToProps函数应该的返回一个纯对象。
-
每一个对象的字段都会作为你的组件的一个独立
prop,并且字段的值通常是一个调用后能分发action的函数。 -
如果你在
dispatch()中使用了action创建函数(区别于纯对象形式的action),通常约定字段名与action创建函数的名称相同const increment = () => ({ type: "INCREMENT" }); const decrement = () => ({ type: "DECREMENT" }); const reset = () => ({ type: "RESET" });
const mapDispatchToProps = dispatch => { return { // 分发由action creators创建的actions 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>
);
}
(Counter案例的完整代码参见:CodeSandbox)
使用bindActionCreators定义mapDispatchToProps函数
手动封装这些函数实在是繁琐,所以Redux提供了一个函数简化这个操作。
bindActionCreators将值为action creators的对象,转化为同键名的新对象,但将每个action creators封装到一个dispatch调用中,以便可以直接调用它们。参阅Redux | bindActionCreators
bindActionCreators接收两个参数:
-
一个函数(action creator)或一个对象(每个属性为一个action creator)
-
dispatch
由bindActionCreators生成的包装函数会自动转发它们所有的参数,所以你不需要在手动操作了。
import { bindActionCreators } from "redux";
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
const reset = () => ({ type: "RESET" });
// 绑定一个action creator
// 返回 (...args) => dispatch(increment(...args))
const boundIncrement = bindActionCreators(increment, dispatch);
// 绑定一个action creators构成的object
const boundActionCreators = bindActionCreators({ increment, decrement, reset }, dispatch);
// 返回值:
// {
// 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);
}
// 组件能接收到 props.increment, props.decrement, props.reset
connect(
null,
mapDispatchToProps
)(Counter);
手动注入dispatch
如果提供了mapDispatchToProps,组件将不再接收到默认的dispatch。但你可以通过在mapDispatchToProps的return中添加dispatch把它重新注入你的组件。多数情况下,你不需要这么做。
import { bindActionCreators } from "redux";
// ...
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ increment, decrement, reset }, dispatch)
};
}
将mapDispatchToProps定义为一个对象
你已经注意到了,在React组件中分发Redux actions的过程都十分类似:定义action创建函数,把它包装在形如(…args) => dispatch(actionCreator(…args))的另一个函数,然后把那个包装函数作为props 传递给你的组件。
因为这一流程实在是太通用了,connect支持了一个“对象简写”形式的mapDispatchToProps参数:如果你传递了一个由action creators构成的对象,而不是函数,connect会在内部自动为你调用bindActionCreators
我们建议适中使用这种“对象简写”形式的**mapDispatchToProps,除非你有特殊理由需要自定义dispatching**行为。
注意到:
-
每个
mapDispatchToProps对象的字段都被假设为一个action创建函数 -
你的组件不再接收
dispatch作为一个prop// React-Redux 自动为你做: 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);
// 或者
export default connect(
mapState,
{ increment, decrement, reset }
)(Counter);
常见问题
为什么组件不再接收dispatch?
也就是说会报错:
TypeError: this.props.dispatch is not a function
在你试图调用this.props.dispatch时这个错误就很常见了,因为实际上dispatch并没有注入到你的组件。
dispatch仅在下面这些情况下注入组件:
-
你没有提供**
mapDispatchToProps**
默认的mapDispatchToProps只是简单的dispatch => ({ dispatch })。如果你不提供mapDispatchToProps,dispatch会以上面的形式自动提供给你。
换言之,也就是你这么做了:
//组件接收 `dispatch`
connect(mapStateToProps /** 没有第二参数*/)(Component);
-
你自定义的**
mapDispatchToProps明确地返回了dispatch**
你也许想把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’s GitHub issue #255.
有关是否需要对组件提供dispatch的讨论(Dan Abramov对#255的回复)。您可以阅读它们以进一步了解目前这么做的目的。
我能不能不使用mapStateToProps而仅使用mapDispatchToProps?
当然。你可以通过给第一个参数传入null或undefined来跳过它。你的组件就不会订阅store但仍然能够接收到mapDispatchToProps定义的dispatch props
connect(
null,
mapDispatchToProps
)(MyComponent);
我可以调用store.dispatch吗?
无论是直接import的还是从context拿到的store,这都不是一种与store交互的良好模式(参见Redux FAQ entry on store setup以获取更多详情)。让React-Redux的connect来获取对store的访问权,并且使用dispatch分发actions。
链接和参考
教程
相关文档
Q&A