连接Redux
Redux 模块准备好了,下面就可以通过 Redux 的 connect 函数把组件和 Redux 的 store 进行连接了。我们以组件 PostList 为例介绍连接过程。
注入state
PostList 组件需要从 Redux 的 store 中获取以下数据:当前登录用户、帖子列表数据和新建帖子编辑框的 UI 状态,根据 store 中 state 的结构,一种最直接的获取所需数据的方式如下:
// containers/PostList/index.js
const getPostList = state => {
return state.posts.allIds.map(id => {
return state.posts.byId[id];
});
};
const mapStateToProps = state => {
return {
user: state.auth, // 当前登录用户
posts: getPostList(state), // 帖子列表数据
isAddDialogOpen: state.ui.addDialogOpen // 新建帖子编辑框的UI状态
};
};
user 和 isAddDialogOpen 两个属性可以直接从 state 中获取,但这种获取方式意味着组件必须了解 state 的结构,而且 state 结构发生变化时,组件也必须通过新的 state 结构访问使用的属性。总之,container 层和 Redux 的 module 层有了强耦合。良好的模块设计对外暴露的应该是模块的接口,而不是模块的具体结构。我们可以利用 Redux 中的 selector 解决这个问题。selector 是一个函数,用于从 state 中获取外部组件所需的数据。这样,当组件需要使用 state 中的数据时,不再直接访问 state,而是通过 selector 获取。上面示例中的 getPostList 就是一个 selector,但 selector 适合定义在相关 Redux 模块中,即一个 Redux 模块不仅包含 action types、action creators 和 reducers,还包含从该模块 state 中获取数据的 selectors。
我们在 auth 模块中定义获取当前用户的 selector:
// redux/modules/auth.js
// selectors
export const getLoggedUser = state => state.auth;
在 ui 模块中定义获取新建帖子编辑框的 UI 状态的 selector:
// redux/modules/ui.js
// selectors
export const isAddDialogOpen = state => {
return state.ui.addDialogOpen;
};
当需要以多个模块的 state 作为 selector 的输入时,这个 selector 就不再适合定义在某个具体模块中,这种情况下,我们定义到 redux/module/index.js 中。例如,posts 模块的 state 只包含作者的 ID 信息,但当展示帖子列表时,需要显示的是作者的用户名,而作者信息需要从 users 模块获取,因此获取帖子列表的 selector 就应该定义在 redux/module/index.js 中,代码如下:
// redux/module/index.js
import { getPostIds, getPostById } from "./posts";
import { getUserById } from "./users";
// complex selectors
export const getPostListWithAuthors = state => {
// 通过 posts 模块的 getPostIds 获取所有帖子的 id
const postIds = getPostIds(state);
return postIds.map(id => {
// 通过 posts 模块的 getPostById 获取每个帖子的详情
const post = getPostById(state, id);
// users模块的 getUserById 获取作者信息,并将作者信息合并到 post 对象中
return {...post, author: getUserById(state, post.author)};
});
};
注意,getPostListWithAuthors 中还使用到了 posts 模块和 users 模块的 selectors。这样通过 selector 进行一些逻辑的处理和数据结构的转换,容器组件可以更加便利地使用全局 state 中的数据。最终,PostList 注入 state 的代码如下:
// containers/PostList/index.js
import { getLoggedUser } from "../../redux/modules/auth";
import { isAddDialogOpen } from "../../redux/modules/ui";
import { getPostListWithAuthors } from "../../redux/modules";
const mapStateToProps = (state, props) => {
return {
user: getLoggedUser(state),
posts: getPostListWithAuthors(state),
isAddDialogOpen: isAddDialogOpen(state)
};
};
注入action creators
接下来为 PostList 注入使用到的 action creators。PostList 中需要获取帖子列表、新建帖子,还需要控制新建帖子编辑框的 UI 状态,因此,PostList 需要使用到 posts 和 ui 两个模块中的 action creators,代码如下:
// containers/PostList/index.js
import {bindActionCreators} from "redux";
import {actions as postActions} from "../../redux/modules/posts";
import {actions as uiActions} from "../../redux/modules/ui";
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(postActions, dispatch),
...bindActionCreators(uiActions, dispatch)
};
};
其中,bindActionCreators 是 Redux 提供的一个工具函数,它使用 store 的 dispatch 方法把参数对象中包含的每个 action creator 包裹起来,这样就不需要显式地使用 dispatch 方法去发送 action 了,而是可以直接调用 action creator 函数(bindActionCreators 返回的对象的属性就是可以直接调用的 action creator)。例如,不使用 bindActionCreators 时,有一个如下定义的 mapDispatchToProps:
const mapDispatchToProps = dispatch => {
return {
someActionCreator: someActionCreator,
};
};
在组件中发起对应的 action 需要这样调用:
this.props.dispatch(this.props.someActionCreator());
mapDispatchToProps 使用 bindActionCreators 后:
const mapDispatchToProps = dispatch => {
return {
someActionCreator: bindActionCreators(someActionCreator, dispatch),
};
};
只需要这样调用即可发送 action:
this.props.someActionCreator();
注意,bindActionCreators 的第一个参数可以是一个函数或者一个普通对象,如果是函数类型,这个函数就是一个 action creator;如果是普通对象类型,对象的每一个属性就是一个 action creator。
connect连接PostList和Redux
最后,利用 Redux 的 connect 函数将 PostList 和 Redux 连接起来,并导出连接后的组件:
// containers/PostList/index.js
import {connect} from "react-redux";
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
完整的 containers/PostList/index.js 代码如下:
import React, {Component} from "react";
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import PostsView from "./components/PostsView";
import PostEditor from "../Post/components/PostEditor";
import {getLoggedUser} from "../../redux/modules/auth";
import {actions as postActions} from "../../redux/modules/posts";
import {actions as uiActions, isAddDialogOpen} from "../../redux/modules/ui";
import {getPostListWithAuthors} from "../../redux/modules";
import "./style.css";
class PostList extends Component {
componentDidMount() {
this.props.fetchAllPosts(); // 获取帖子列表
}
// 保存帖子
handleSave = data => {
this.props.createPost(data.title, data.content);
};
// 取消新建帖子
handleCancel = () => {
this.props.closeAddDialog();
};
// 新建帖子
handleNewPost = () => {
this.props.openAddDialog();
};
render() {
const {posts, user, isAddDialogOpen} = this.props;
return (
<div className="postList">
<div>
<h2>话题列表</h2>
{user.userId ? (
<button onClick={this.handleNewPost}>发帖</button>
) : null}
</div>
{isAddDialogOpen ? (
<PostEditor onSave={this.handleSave} onCancel={this.handleCancel}/>
) : null}
<PostsView posts={posts}/>
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
user: getLoggedUser(state),
posts: getPostListWithAuthors(state),
isAddDialogOpen: isAddDialogOpen(state)
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(postActions, dispatch),
...bindActionCreators(uiActions, dispatch)
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
其他容器组件和 Redux 的连接方式与 PostList 相同,区别只是注入的 state 和 action creators 不同。限于篇幅,这里不再一一介绍。
最后,我们还需要把 Redux 的 store 通过 Provider 组件注入应用中,这个操作在应用的根组件中完成:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import configureStore from "./redux/configureStore";
import App from "./containers/App";
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
大功告成!完整的代码可参考源代码。