以Favorite组件为例分析 RN+Redux 状态管理与数据流

无论使用React还是ReactNative,Redux总是绕不过的结(劫?解?)。近日在实现一个本地收藏组件的时候,浅显但还算完整的使用了Redux来管理收藏的状态与同步,因而有了本文(文末有demo视频)。

0 准备

先上个参考文献甩锅。我讲不清的,请查看参考文献,还有个小Sample搭配

1 需求

Favorite组件,本文主角,其实就是一个收藏按钮(图1)。用户点击按钮,按钮变实心,收藏此篇文章,将这篇文章加入收藏列表(图3),同时在所有显示这篇文章的地方,自动同步收藏状态。为简单描述,省略server交互过程,我们假设收藏文章的信息都存在本地。

  • 这么lowbe的组件干嘛要用redux?: 因为有同步需求!我们需要做到一处收藏,处处‘亮星’,任意页面收藏一篇文章,任何其他地方,即使是已经渲染好的父页面,也同步此篇文章的收藏状态——亮星或灭星。(例如:图2中详情页是由图1中的列表页点击进入的,如果用户在详情页看完文章,点击收藏,返回回来时,列表页也会同步状态)
    图1-列表页
图2-文章详情页
图3-收藏列表

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 转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容