无论使用React还是ReactNative,Redux总是绕不过的结(劫?解?)。近日在实现一个本地收藏组件的时候,浅显但还算完整的使用了Redux来管理收藏的状态与同步,因而有了本文(文末有demo视频)。
0 准备
先上个参考文献甩锅。我讲不清的,请查看参考文献,还有个小Sample搭配
1 需求
Favorite组件,本文主角,其实就是一个收藏按钮(图1)。用户点击按钮,按钮变实心,收藏此篇文章,将这篇文章加入收藏列表(图3),同时在所有显示这篇文章的地方,自动同步收藏状态。为简单描述,省略server交互过程,我们假设收藏文章的信息都存在本地。
-
这么lowbe的组件干嘛要用redux?: 因为有同步需求!我们需要做到一处收藏,处处‘亮星’,任意页面收藏一篇文章,任何其他地方,即使是已经渲染好的父页面,也同步此篇文章的收藏状态——亮星或灭星。(例如:图2中详情页是由图1中的列表页点击进入的,如果用户在详情页看完文章,点击收藏,返回回来时,列表页也会同步状态)
图1-列表页
2 实现
因为需求中明显涉及到跨组件状态的同步,所以用redux也就是很自然的了,react配合redux通常需要实现“四大金刚”:Action,Reducer,Container,Component,下面一一道来。
-
Action: 顾名思义,是一些动作的定义,因为redux这一类的状态管理方式强调单向数据流与可追踪,因此使用redux管理的数据,必须通过dispatch某一action来修改,这可以保证任意对于数据的修改都是可追踪的,且一定是通过action这个入口进入的。
如本例:定义两个动作,ADD_FAVORITE和REMOVE_FAVORITE,当用户点击收藏按钮,dispatch增加;在已收藏的按钮上点击,dispatch删除。但是,请注意Action仅仅是定义,还未对数据真正进行修改,修改是下面那哥们的活儿。就好比皇帝饿了要吃肉,他(用户)大喊一声:我要吃肉,这只是先下了圣旨(action),但是后厨(reducer)还没开始做呢!
import * as types from '../constants/ActionTypes';
export function addFavorite(article) {
return {
type: types.ADD_FAVORITE,//常量定义文件中定义好的常量字符串
article//收藏的文章object,{id:123,title:'hello',....}
};
}
export function removeFavorite(article) {
return {
type: types.REMOVE_FAVORITE,
article
};
}
-
Reducer:reducer但从字面不好理解,但是其实可以将其理解为一个action的具体执行过程, reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state:
(previousState, action) => newState
,就是这么简单,一点儿都不恐怖对不对?请注意,针对Reducer,保持其纯净的计算属性非常重要,所以请谨记永远不要在 reducer 里做有副作用的或异步的一些操作,参考这儿。- 新的state: 请务必注意是新的state,引用地址要变,而不要拿着一个引用地址在那儿狂赋值(我就做过),尤其针对子对象,子数组对象的元素增删。原因主要是方便react监听数据的变动,否则极有可能无法触发组件的更新。
- 调用api这一类怎么办:多写几个action,发起api调用一个action;成功返回一个action,错误返回一个action,应该豁然开朗了吧?
import * as types from '../constants/ActionTypes';
import * as _ from 'lodash'
const initialState = {
favoriteItems:[]//存储用户收藏的article列表,这一行只是设初值
};
//Reducer主体:很纯粹的一个函数,接受老的state和action,返回新的state
export default function favorite(state = initialState, action) {
switch (action.type) {
case types.ADD_FAVORITE://收藏时对应的操作,将action带过来的article加到列表中,仔细看此处的操作,返回的是《新的》state
return Object.assign({}, state, {
favoriteItems: insertItem(state.favoriteItems, action.article)
});
case types.REMOVE_FAVORITE://相对应的,删除操作
return Object.assign({}, state, {
favoriteItems: removeItem(state.favoriteItems, action.article)
});
default:
return state;
}
}
//这两个工具函数就是为了让我们在每次数据更新时,返回的都是全新的article列表
function insertItem(array, item) {
let newArray = array.slice();
newArray.splice(0, 0, item);
return newArray;
}
function removeItem(array, item) {
let newArray = array.slice();
_.remove(newArray,{id: item.id});
return newArray;
}
-
container & component :这两个应该是独立的部分,此处写到一起是因为我在实现时代码放到一起了,但是其职责完全不同:
- container:容器组件,连接数据与展示组件的桥梁,主要做的就是把store的数据和action注入到展示组件中。
- component:展示组件,这个不多讲了,就是我们的普通组件,本例中这个组件内部就是画了一个星星状的按钮。
import React, {PropTypes} from 'react';
import Icon from 'react-native-vector-icons/Ionicons';
import * as _ from 'lodash';
import ToastUtil from "../utils/ToastUtil";
import * as COLOR from "../constants/Colors";
import * as creaters from '../actions/favorite';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
//容器组件接受的props
const propTypes = {
clickedName: PropTypes.string,
unClickedName: PropTypes.string,
favoriteItems: PropTypes.array,//这是个特殊的props,来源于redux store,下面会看到,这个是自动注入的
article: PropTypes.object
};
//展示组件定义
class FavoriteIcon extends React.Component {
constructor(props) {
super(props);
this.state = {
iconName: ''
};
}
/*请注意,这儿针对组件渲染做了一点儿性能优化,因为本例中在任何收藏按钮上点击,都将修改
FavoriteItems这个list,而只要这个list修改,就会触发所有收藏按钮的重新渲染判断,这是不必要的,所以
此处针对自己是否在新旧FavoriteItems做了一个异或,只有异或结果为TRUE,才表示需要update
*/
shouldComponentUpdate(nextProps, nextState) {
if (this.props.article !== nextProps.article){
return true;
}
return (_.some(nextProps.favoriteItems, {id: this.props.article.id}) ^
_.some(this.props.favoriteItems, {id: this.props.article.id}))
}
render() {
let {clickedName, unClickedName, favoriteItems, article, favoriteActions} = this.props;
return (
<Icon.Button
name={_.some(favoriteItems, {id: article.id}) ? clickedName : unClickedName}//显示实心的已收藏还是空心的未收藏
backgroundColor="transparent"
underlayColor="transparent"
color={COLOR.HeaderText}
activeOpacity={0.8}
onPress={() => {
if (_.some(favoriteItems, {id: article.id})) {
favoriteActions.removeFavorite(article);//关键一步:我们在此处调用了注入进来的action,dispatch了一个remove favorite action
ToastUtil.showShort('Article removed from favorite');
} else {
favoriteActions.addFavorite(article);// 上同,dispatch add action
ToastUtil.showShort('Article marked as favorite');
}
}}
/>
)
}
}
// 容器组件定义,可以看到,这个组件什么都没做,只是引用了展示组件,并且把props穿进去,很好理解吧?
class Container extends React.Component {
render() {
return <FavoriteIcon {...this.props}/>
}
}
//关键性操作,将redux store中的favoriteItems 注入到容器组件的props中
const mapStateToProps = (state) => {
const {favoriteItems} = state.favorite;
return {
favoriteItems
};
};
//关键性操作,将redux store中操作favoriteitems的action注入到容器组件的props中
const mapDispatchToProps = (dispatch) => {
const favoriteActions = bindActionCreators(creaters, dispatch);
return {
favoriteActions
};
};
Container.propTypes = propTypes;
Container.defaultProps = {
clickedName: "ios-star",
unClickedName: "ios-star-outline"
};
//此处用react-redux的connect生成容器组件,并且把相关的注入处理好,大功告成。
// 此时你就可以直接用这个容器组件了,就像用普通展示组件一样,但是区别是,props里面会自动注入redux store中的相关data和action。
//只要redux store中data一变,props中相关数据就会变,从而自动触发试图更新。组件中的componentWillReceiveProps 也会触发。
export default connect(mapStateToProps, mapDispatchToProps)(Container);
- Finally,开心的用吧
<View>
...
<FavoriteIcon article={article}/>// 记得传入article对象哦
...
</View>
3 写在最后
如果你有全部看完代码实现逻辑,细心的你应该会发现,我有在展示组件里面做渲染性能优化,其实这是不得已而为之,因为整套组件的设计架构导致了每次的收藏都会导致store中favoriteitem列表的变化,而这个变化会导致所有icon的props变化,进而重渲染。此处用shouldComponentUpdate做过滤虽然避免了vitual dom比较的开销,但是这个函数本身也有计算开销,而且,virtual dom diff过程和此方法的执行开销孰大孰小可能也要打个问号。在此我能想到的一个优化方式是将user对于一个article的收藏状态临时存于article,借助article的更新来refresh任意位置的收藏状态。当然这需要做更多的操作,比如每次网络获取articlelist之后,都需要与本地favoriteList做merge,给已经收藏的文章打一个标记。所以,这是一个折中的过程,如果同时渲染的favorite icon数量不多,其实本文实现方式足够了,也欢迎大家在评论区就优化方法留言讨论 :)
另外,细心地你应该还会发现一个问题,favoriteItems没有持久化?用户关闭软件再进来岂不是就没了?没错,这个地方是需要持久化的,best practice自然是持久化到server,但是此处我们只持久化到了phone本地存储,借助的是redux-persist,傻瓜式替我们做这一步,大概代码如下:
const middlewares = [];
middlewares.push(...);//你的其他中间件
export default function configureStore() {
const store = createStore(
rootReducer,
undefined,
compose(
applyMiddleware(...middlewares),
autoRehydrate()//magic 一般的帮我们统统的持久化了
)
);
store.close = () => store.dispatch(END);
persistStore(store, {storage: AsyncStorage});//用rn提供的AsyncStore做save 引擎
return store;
}
The End ,欢迎留言讨论
f95f5d7455643e7543ae218bfae8b0bc.gif原文链接:http://www.jianshu.com/p/c925e84ec06a
作者: changchao 转载请注明出处
网友评论