在React中使用Redux
安装react-redux
首先需要强调,Redux 和 React 并无直接关联,Redux 可以和很多库一起使用。为了方便在 React 中使用 Redux,我们需要使用 react-redux 这个库。这个库并不包含在 Redux 中,所以需要单独安装:
npm install react-redux
展示组件和容器组件
根据组件意图的不同,可以将组件划分为两类:展示组件(presentational components)和容器组件(container components)。
展示组件负责应用的 UI 展示(how things look),也就是组件如何渲染,具有很强的内聚性。展示组件不关心渲染时使用的数据是如何获取到的,它只要知道有了这些数据后,组件应该如何渲染就足够了。数据如何获取是容器组件负责的事情。
容器组件负责应用逻辑的处理(how things work),如发送网络请求、处理返回数据、将处理过的数据传递给展示组件使用等。容器组件还提供修改源数据的方法,通过展示组件的 props 传递给展示组件,当展示组件的状态变更引起源数据变化时,展示组件通过调用容器组件提供的方法同步这些变化。
展示组件和容器组件可以自由嵌套,一个容器组件可以包含多个展示组件和其他的容器组件;一个展示组件也可以包含容器组件和其他的展示组件。这样的分工可以使与 UI 渲染无直接关系的业务逻辑由容器组件集中负责,展示组件只关注 UI 的渲染逻辑,从而使展示组件更容易被复用。对于非常简单的页面,一般只需要一个容器组件就足够了;但对于复杂的页面,往往需要多个容器组件,否则所有的业务逻辑都在一个容器组件中处理的话,会导致这个组件非常复杂,同时这个组件获取到的源数据可能需要经过很多层组件 props 的传递才能到达最终使用的展示组件。
展示组件和容器组件容易和 2.2.4 小节介绍的无状态组件和有状态组件混淆。这两组概念对组件的划分依据是不同的。展示组件和容器组件是根据组件的意图划分组件,无状态组件和有状态组件是根据组件内部是否使用 state 划分组件。不过通常情况下,展示组件是通过无状态组件来实现的,容器组件是通过有状态组件来实现的,但是展示组件也可以是有状态组件,容器组件也可以是无状态组件。 |
connect
react-redux 提供了一个 connect 函数,用于把 React 组件和 Redux 的 store 连接起来,生成一个容器组件,负责数据管理和业务逻辑,代码如下:
import { connect } from 'react-redux'
import TodoList from './TodoList'
const VisibleTodoList = connect()(TodoList);
这里创建了一个容器组件 VisibleTodoList,可以把组件 TodoList 和 Redux 连接起来。但是,这个 VisibleTodoList 只是一个空壳,并没有负责任何真正的业务逻辑。根据 Redux 的数据流过程,VisibleTodoList 需要承担两个工作:
-
从 Redux 的 store 中获取展示组件所需的应用状态。
-
把展示组件的状态变化同步到 Redux 的 store 中。
通过为 connect 传递两个参数可以让 VisibleTodoList 具备这两个功能:
import { connect } from 'react-redux'
import TodoList from './TodoList'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
mapStateToProps 和 mapDispatchToProps 的类型都是函数,前者负责从全局应用状态 state 中取出所需数据,映射到展示组件的 props,后者负责把需要用到的 action 映射到展示组件的 props 上。
mapStateToProps
mapStateToProps 是一个函数,从名字就可以看出,它的作用是把 state 转换成 props。state 就是 Redux store 中保存的应用状态,它会作为参数传递给 mapStateToProps,props 就是被连接的展示组件的 props。例如,VisibleTodoList 需要根据 state 中的 todos 和 visibilityFilter 两个数据过滤出传递给 TodoList 的待办事项数据:
function getVisibleTodos(todos, filter) {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
function mapStateToProps(state) {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
每当 store 中的 state 更新时,mapStateToProps 就会重新执行,重新计算传递给展示组件的 props,从而触发组件的重新渲染。
store 中的 state 更新一定会导致 mapStateToProps 重新执行,但不一定会触发组件 render 方法的重新执行。如果 mapStateToProps 新返回的对象和之前的对象浅比较(shallow comparison)相等,组件的 shouldComponentUpdate 方法就会返回 false,组件的 render 方法也就不会被再次触发。这是 react-redux 库的一个重要优化。 |
connect 可以省略 mapStateToProps 参数,这样 state 的更新就不会引起组件的重新渲染。
mapStateToProps 除了接收 state 参数外,还可以使用第二个参数,代表容器组件的 props 对象,例如:
// ownProps 是组件的 props 对象
function mapStateToProps(state, ownProps) {
//...
}
mapDispatchToProps
容器组件除了可以从 state 中读取数据外,还可以发送 action 更新 state,这就依赖于 connect 的第二个参数 mapDispatchToProps。mapDispatchToProps 接收 store.dispatch 方法作为参数,返回展示组件用来修改 state 的函数,例如:
// toggleTodo(id) 返回一个 action
function toggleTodo(id) {
return { type: 'TOGGLE_TODO', id }
}
function mapDispatchToProps(dispatch) {
return {
onTodoClick: function (id) {
dispatch(toggleTodo(id))
}
}
}
这样,展示组件内就可以调用 this.props.onTodoClick(id) 发送修改待办事项状态的 action 了。另外,与 mapStateToProps 相同,mapDispatchToProps 也支持第二个参数,代表容器组件的 props。
Provider组件
通过 connect 函数创建出容器组件,但这个容器组件是如何获取到 Redux 的 store?react-redux 提供了一个 Provider 组件,Provider 的部分示意代码如下(为了方便理解,这里的代码和 react-redux 中的代码并不完全一致):
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
provider.childContextTypes = {
sotre: React.PropTypes.object
}
Provider 组件需要接收一个 store 属性,然后把 store 属性保存到 context(如果忘记 context 的用法,可参考4.3节组件通信)。Provider 组件正是通过 context 把 store 传递给子组件的,所以使用 Provider 组件时,一般把它作为根组件,这样内层的任意组件才可以从 context 中获取 store 对象,代码如下:
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)