react-redux

174 阅读16分钟

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) => { } )** 的形式定义的——它都能够以同样的方式生效。

参数

  1. state

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

链接和参考

教程

性能

Q&A

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。你对dispatchownProps都具有访问权限。你可以借此机会编写你的连接组件的自定义函数。

参数

  1. dispatch

  2. 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接收两个参数:

  1. 一个函数(action creator)或一个对象(每个属性为一个action creator)

  2. 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。但你可以通过在mapDispatchToPropsreturn中添加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仅在下面这些情况下注入组件:

  1. 你没有提供**mapDispatchToProps**

默认的mapDispatchToProps只是简单的dispatch => ({ dispatch })。如果你不提供mapDispatchToPropsdispatch会以上面的形式自动提供给你。

换言之,也就是你这么做了:

//组件接收 `dispatch`
connect(mapStateToProps /** 没有第二参数*/)(Component);
  1. 你自定义的**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?

当然。你可以通过给第一个参数传入nullundefined来跳过它。你的组件就不会订阅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