1. reselect简介
reselect是一个与redux配合使用的一个模块,它可根据state上的多个已有的属性来生成一个新的属性,
这可以为简化代码逻辑带来很大的帮助。比如我们在state上有users这个属性,而在每个user上又有性别
这个属性,那么我们可以使用reselect来为我们分别生成性别为男和女的新属性,而且也不需要加入新的
reducer和action。
2. 一个简单的例子
现在我们有一个简单的文章系统,大概是这样的:

文件目录大概这样:
1 | |- actions/ |
代码:
/index.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import App from './components/app';
import reducers from './reducers';
const createStoreWithMiddleware = applyMiddleware(
reduxThunk
)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<App />
</Provider>
, document.querySelector('.container'));
/components/app.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import React, { Component } from 'react';
import PostSelector from './containers/post_selectors';
import PostList from './containers/post_list'
export default class App extends Component {
render() {
return (
<div>
<PostList />
<PostSelector />
</div>
);
}
}
/components/containers/post_selectors.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'
import { getPosts, selectPost, deselectPost } from '../../actions/index';
class PostSelector extends Component {
componentWillMount(){
this.props.getPosts();
}
handleCheck(postId, event){
if(event.target.checked){
this.props.selectPost(postId)
}else{
this.props.deselectPost(postId)
}
}
renderPostList(){
return this.props.posts.map(post => {
return (
<li className="list-group-item" key={post.id}>
<input
type="checkbox"
value={this.props.selectedIds.includes(post.id)}
onChange={this.handleCheck.bind(this, post.id)}
/>
{post.title}
</li>
)
})
}
render(){
//console.log(this.props.posts)
if(this.props.posts){
return (
<div>
<h2>文章列表</h2>
<ul className="list-group">
{this.renderPostList()}
</ul>
</div>
)
}else{
return <h2>Loading...</h2>
}
}
}
function mapStateToProps(state){
return {
posts: state.posts,
selectedIds: state.selectedIds
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators({
getPosts,
selectPost,
deselectPost
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(PostSelector);
/components/containers/post_list.js 1
2
3
4
5
6
7
8
9
10
11import React, { Component } from 'react';
export default class PostList extends Component {
render(){
return (
<div>
<h2>已选中的文章</h2>
</div>
)
}
}
/actions/types.js 1
2
3export const GETPOSTS = 'GETPOSTS';
export const SELECTPOST = 'SELECTPOST';
export const DESELECTPOST = 'DESELECTPOST';
/actions/index.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import axios from 'axios';
import _ from 'lodash';
import { GETPOSTS, SELECTPOST, DESELECTPOST } from './types';
export function getPosts(){
return dispatch => {
const fetchUrl = 'http://jsonplaceholder.typicode.com/posts';
axios.get(fetchUrl).then(response=>{
if(response.status == 200){
dispatch({
type: GETPOSTS,
payload: response.data.slice(0, 5)
})
}
})
}
}
export function selectPost(postId){
return dispatch => {
dispatch({
type: SELECTPOST,
payload: postId
})
}
}
export function deselectPost(postId){
return {
type: DESELECTPOST,
payload: postId
}
}
/reducers/index.js 1
2
3
4
5
6
7
8
9
10import { combineReducers } from 'redux';
import all_post_reducer from './all_post_reducer';
import select_post_reducer from './select_post_reducer';
const rootReducer = combineReducers({
posts: all_post_reducer,
selectedIds: select_post_reducer
});
export default rootReducer;
/reducers/all_post_reducer.js 1
2
3
4
5
6
7
8
9
10
11import { GETPOSTS, SELECTPOST, DESELECTPOST } from '../actions/types';
export default function(state=[], action){
switch(action.type){
case GETPOSTS:
return action.payload;
default:
return state;
}
}
/reducers/select_post_reducer.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14import { GETPOSTS, SELECTPOST, DESELECTPOST } from '../actions/types';
export default function(state=[], action){
switch(action.type){
case SELECTPOST:
return [...state, action.payload];
case DESELECTPOST:
return state.filter(id=> id!=action.payload);
default:
return state;
}
}
代码概述:
这是一个很简单的react-redux项目,通过两个action creator来向reducer发送action,并且
生成新的state,这里state中共有两个属性,1,包含所有文章的数组,这里可以看出来,我们使用的是jsonplaceholder
中的post并且只取了前5个。2,包含所有已经被选的文章Id数组,这个数组只包含文章的Id,大概是这样的[1, 2, 4]
。用户可以通过选择或反选文章左侧的checkbox来进行选择或者反选文章,其实就是往被选文章数组内
添加当前文章Id或者是从被选文章数组内移除当前文章的Id。
一点题外话
我在写这个小例子的时候除了个小状况,而且,我觉得应该很多人都出过这种状况吧。当时我差不多写好的时候,
发现不管是选择还是反选文章前的checkbox,被选文章的数组始终是个空数组,经过一番排查,发现错误很诡异
action creator方法确实执行了,但是reducer却没有反应,而且浏览器没有报任何错误,换句话说,reducer
没有接收到action creator发出的action。经过长(da)时(shen)间(de)调(zhi)试(dian),最终发现bug的所在:
我在组件中调用actioncreator 方法的时候忘了加 this.props 这么做会导致在action creator被
调用的时候,它调用的不是props上的那个redux包装过的action creator,而是调用的import进来的那个包装前的。
要知道,在bindActionCreator执行的时候,里面的方法已经跟redux绑定在了一起,但是之前那个import进来的
只是一个纯函数,可以执行,然后return一个action object,并不会dispatch给reducer。这个错误我觉的应该
不是我一个人犯过,所以大家看了以后,如果你的action creator可以正常以运行但是reducer却接不到任何一个
action,而且浏览器不报任何错误,那么多半就是这个原因了,检查一下你在运行action creator的时候是不是
忘了加this.props吧。
3. 补全post_list
可以看出来,post_list根本就没写完,目前,它只能显示一个h2标签。我们需要它在有文章被勾选时显示这个文章,
再文章被反选时把这个文章移除。那么很明显,它需要是一个Smart Component,就是说它需要和state tree相关联。
state tree已绑定的属性
目前我们的state tree上已经绑定了这些:
- 所有的文章
- 一个包含已选中文章Id的数组
那么目前看来,单纯用redux来解决的话大概有两个解决方案:
在state tree上新增加一个数组,里面包含所有被选中的文章(不是只有文章Id),然后把这个属性绑在post_list上。
这样一来我们需要重写post_selectors.js这个组件,在选中或者反选时调用一个新的action creator,而这个action
creator的作用就是根据参数传入的Id来对所有文章的数组进行过滤,把Id为参数传入的那个文章作为payload传给reducer,
最后reducer把那个文章加入到新书组内并且返回新的数组。在post_list的componentWillMount时对所有文章的数组进行过滤,只保留Id包含在被选文章Id数组内的文章,然后对
其进行渲染。
问题
这两种解决办法都有一定的问题,第一种做法使得逻辑变得复杂,因为要在state上加入新的属性,并且需要加入新的
action creator以及reducer。第二种方法虽然看似简单,但是却使得组件的复用性变差了,如果我之后想做一个新的post_list
里面只显示没有选中的文章,那么就没办法服用这个组件,因为componentWillMount方法的逻辑不同。
4. 解决方案
在redux生态系统中,有这么个东西,叫做reselect,GitHub 连接,这个
模块能很好的解决我们目前的问题。首先,我们知道post_list上需要的文章并不是新的文章,而是由所有文章和已选中文章Id的
数组进行过滤后得到的。如果state tree上的某个属性是由其他属性进行某种逻辑运算后得到的,那么我们就可以考虑使用reselect。
使用reselect
首先为了让文件目录更清晰,我们在根目录下新增加一个文件夹,叫selectors用来存放所有的selector,至于selector是什么,
我们马上就知道了,目前的文件目录大概是这样:
1 | |- actions/ |
/selectors/select_post_selector.js 1
2
3
4
5
6
7
8
9
10import { createSelector } from 'reselect';
const allPostSelector = state => state.posts;
const selectedIdSelector = state => state.selectedIds;
const getSelectedPosts = (posts, selectedIds) => {
return posts.filter(post => selectedIds.includes(post.id));
};
export default createSelector(allPostSelector, selectedIdSelector, getSelectedPosts);
大概说一下,allPostSelector一个函数,它会在state tree上找到posts这个属性,也就是所有文章的数组,
selectedIdSelector和allPostSelector类似,会在state tree上找到selectedIds属性,也就是那个
包含所有被选文章Id的数组。
getSelectedPosts也是一个函数,它接收多个参数,然后返回根据这些参数生成的新属性,这个属性最后会绑定
到state tree上。
然后我们需要在post_list这一组件中使用这个用reselect生成的新属性
/components/containers/post_list.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import React, { Component } from 'react';
import { connect } from 'react-redux';
import PostSelector from '../../selectors/select_post_selector';
class PostList extends Component {
renderPostLists(){
return this.props.selectedPosts.map(post=>{
return (
<li className="list-group-item" key={post.id}>{post.title}</li>
)
})
}
render(){
return (
<div>
<h2>已选中的文章</h2>
<ul className="list-group">
{this.renderPostLists()}
</ul>
</div>
)
}
}
function mapStateToProps(state){
return {
selectedPosts: PostSelector(state)
}
}
export default connect(mapStateToProps)(PostList);
这个组件其实基本和使用redux对state进行绑定后的Smart组件没什么区别,唯一一点值得
注意的是第30行,selectedPosts: PostSelector(state)
这里。我们只是用刚才写好
的selector对state进行加工,用已有的属性(所有post和被选postId)来生成这个新的属性。
5. 小结
运行一下,一切都没问题,而且这样一来,post_list这个组件也可以很好的复用了。值得注意的
是,现在这个小例子只是为了向大家简单介绍一下reselect这个模块,或许你感觉不到使用reselect
的必要性,但是,要知道这个例子中的state tree结构非常简单,但是当在实际开发中,业务逻辑往往
会变得十分复杂,这也就导致了state tree结构也会变得复杂,通过这种方式来简化state tree将会
是一种非常好的优化手段。