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

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

0 准备

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

1 需求

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

  • 这么lowbe的组件干嘛要用redux?: 因为有同步需求!我们需要做到一处收藏,处处‘亮星’,任意页面收藏一篇文章,任何其他地方,即使是已经渲染好的父页面,也同步此篇文章的收藏状态——亮星或灭星。(例如:图2中详情页是由图1中的列表页点击进入的,如果用户在详情页看完文章,点击收藏,返回回来时,列表页也会同步状态)
    以Favorite组件为例分析 RN+Redux 状态管理与数据流_第1张图片
    图1-列表页
以Favorite组件为例分析 RN+Redux 状态管理与数据流_第2张图片
图2-文章详情页
以Favorite组件为例分析 RN+Redux 状态管理与数据流_第3张图片
图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:展示组件,这个不多讲了,就是我们的普通组件,本例中这个组件内部就是画了一个星星状的按钮。
以Favorite组件为例分析 RN+Redux 状态管理与数据流_第4张图片
两种组件对比
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 (
             {
                    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 
    }
}
//关键性操作,将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,开心的用吧

  ...
  // 记得传入article对象哦
  ...


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

你可能感兴趣的:(以Favorite组件为例分析 RN+Redux 状态管理与数据流)