React Native实现一个带筛选功能的搜房列表(2)

原文链接React Native实现一个带筛选功能的搜房列表(2)

上一篇中,我们实现了一个下拉刷新和上拉加载更多的列表,那根据一般的开发步骤,接着应该就是进行网络请求,在网络请求之后更新列表数据和列表的刷新状态。

这篇文章会向大家介绍一下Redux的基本概念以及在页面中如何使用Redux进行状态管理。

文章中的代码都来自代码传送门--NNHybrid

Redux概念

首先先简单介绍一下Redux的一些概念。Redux是JavaScript状态容器,提供可预测化的状态管理,其工作流程如下:

整个工作流程为:


reduxProcess
  1. View需要订阅Store中的state;
  2. 操作View(点击了View上的一个按钮或者执行一个网络请求),发出Action;
  3. Store自动调用Reducer,并且传入两个参数(Old State和Action),Reducer会返回新的State,如果有Middleware,Store会将Old State和Action传递给Middleware,Middleware会调用Reducer 然后返回新的State;
  4. State一旦有变化,Store就会调用监听函数,来更新View;

Store

Store是存储state的容器,负责提供所有的状态。整个应用只能有一个Store,这么做的目的是为了让组件之间的通信更加简单。

reduxCommunication

在没有Store的情况下,组件之间需要通信就比较麻烦,如果一个父组件要将状态传递到子组件,就需要通过props一层一层往下传,一个子组件的状态发生改变并且要让父组件知道,则必须暴露一个事件出去才能通信。这就使得组件之间通信依赖于组件的层次结构。此时如果有两个平级的节点想要通信,就需要通过它们的父组件进行中转。
有了这个全局的Store之后,所有的组件变成了和Store进行通信。这样组件之间通信就会变少,当Store发生变化,对应的组件也能拿到相关的数据。当组件内部有时间触发Store的变化时,更新Store即可。这也就是所谓的单向数据流过程。

Store的职责如下:

  • 维持应用的state;
  • 提供getState()方法获取state;
  • 提供dispatch(action)方法更新state;
  • 通过subscribe(listener)注册监听器;
  • 通过subscribe(listener)返回的函数注销监听器。

Action

当我们想要更改store中的state时,我们便需要使用Action。Action是Store数据的唯一来源,每一次修改state便要发起一次Action。

Action可以理解为是一个Javascript对象。其内部必须包含一个type字段来表示将要执行的动作,除了 type字段外,Action的结构完全由自己决定。多数情况下,type字段会被定义成字符串常量。

Action举例:

{
    type: Types.SEARCH_HOUSE_LOAD_DATA_SUCCESS,
    currentPage: ++currentPage,
    houseList,
    hasMoreData,
}

Action创建函数

Action创建函数就是生成action的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。

Action创建函数举例:

export function init(storeName) {
    return dispatch => {
        dispatch({ type: Types.HOUSE_DETAIL_INIT, storeName });
    }
}

Reducer

Store收到Action以后,必须给出一个新的State,这样View才会发生变化。
这种State的计算过程就叫做Reducer。Reducer是一个纯函数,它只接受Action和当前State作为参数,返回一个新的State。

由于Reducer是一个纯函数,所以我们不能在reducer里执行以下操作:

  • 修改传入的参数;
  • 执行有副作用的操作;
  • 调用非纯函数;
  • 不要修改state;
  • 遇到未知的action时,一定要返回旧的state;

Reducer举例:

const defaultState = {
    locationCityName: '',
    visitedCities: [],
    hotCities: [],
    sectionCityData: [],
    sectionTitles: []
};

export function cityListReducer(state = defaultState, action) {
    switch (action.type) {
        case Types.CITY_LIST_LOAD_DATA:
            return {
                ...state,
                visitedCities: action.visitedCities,
                hotCities: action.hotCities,
                sectionCityData: action.sectionCityData,
                sectionTitles: action.sectionTitles,
            }
        case Types.CITY_LIST_START_LOCATION:
        case Types.CITY_LIST_LOCATION_FINISHED:
            return {
                ...state,
                locationCityName: action.locationCityName
            };
        default:
            return state;
    }

}

拆分与合并reducer

在开发过程中,由于有的功能是相互独立的,所以我们需要拆分reducer。一般情况下,针对一个页面可以设置一个reducer。但redux原则是只允许一个根reducer,接下来我们需要将每个页面的的reducer聚合到一个根reducer中。

合并reducer代码如下:

const appReducers = combineReducers({
    nav: navReducer,
    home: homeReducer,
    cityList: cityListReducer,
    apartments: apartmentReducer,
    houseDetails: houseDetailReducer,
    searchHouse: searchHouseReducer,
});

export default (state, action) => {
    switch (action.type) {
        case Types.APARTMENT_WILL_UNMOUNT:
            delete state.apartments[action.storeName];
            break;
        case Types.HOUSE_DETAIL_WILL_UNMOUNT:
            delete state.houseDetails[action.storeName];
            break;
        case Types.SEARCH_HOUSE_WILL_UNMOUNT:
                delete state.searchHouse;
            break;
    }

    return appReducers(state, action);
}

SearchHousePage使用Redux

Action类型定义

SEARCH_HOUSE_LOAD_DATA: 'SEARCH_HOUSE_LOAD_DATA',
SEARCH_HOUSE_LOAD_MORE_DATA: 'SEARCH_HOUSE_LOAD_MORE_DATA',
SEARCH_HOUSE_LOAD_DATA_SUCCESS: 'SEARCH_HOUSE_LOAD_DATA_SUCCESS',
SEARCH_HOUSE_LOAD_DATA_FAIL: 'SEARCH_HOUSE_LOAD_DATA_FAIL',
SEARCH_HOUSE_WILL_UNMOUNT: 'SEARCH_HOUSE_WILL_UNMOUNT',

Action创建函数

export function loadData(params, currentPage, errorCallBack) {
    return dispatch => {
        dispatch({ type: currentPage == 1 ? Types.SEARCH_HOUSE_LOAD_DATA : Types.SEARCH_HOUSE_LOAD_MORE_DATA });

        setTimeout(() => {
            Network
                .my_request({
                    apiPath: ApiPath.SEARCH,
                    apiMethod: 'searchByPage',
                    apiVersion: '1.0',
                    params: {
                        ...params,
                        pageNo: currentPage,
                        pageSize: 10
                    }
                })
                .then(response => {
                    const tmpResponse = AppUtil.makeSureObject(response);
                    const hasMoreData = currentPage < tmpResponse.totalPages;
                    const houseList = AppUtil.makeSureArray(tmpResponse.resultList);
                    dispatch({
                        type: Types.SEARCH_HOUSE_LOAD_DATA_SUCCESS,
                        currentPage: ++currentPage,
                        houseList,
                        hasMoreData,
                    });
                })
                .catch(error => {
                    if (errorCallBack) errorCallBack(error.message);

                    const action = { type: Types.SEARCH_HOUSE_LOAD_DATA_FAIL };
                    if (currentPage == 1) {
                        action.houseList = []
                        action.currentPage = 1;
                    };

                    dispatch(action);
                });
        }, 300);
    }
}

创建reducer

// 默认的state
const defaultState = {
    houseList: [],
    headerIsRefreshing: false,
    footerRefreshState: FooterRefreshState.Idle,
    currentPage: 1,
}

export function searchHouseReducer(state = defaultState, action) {
    switch (action.type) {
        case Types.SEARCH_HOUSE_LOAD_DATA: {
            return {
                ...state,
                headerIsRefreshing: true
            }
        }
        case Types.SEARCH_HOUSE_LOAD_MORE_DATA: {
            return {
                ...state,
                footerRefreshState: FooterRefreshState.Refreshing,
            }
        }
        case Types.SEARCH_HOUSE_LOAD_DATA_FAIL: {
            return {
                ...state,
                headerIsRefreshing: false,
                footerRefreshState: FooterRefreshState.Failure,
                houseList: action.houseList ? action.houseList : state.houseList,
                currentPage: action.currentPage,
            }
        }
        case Types.SEARCH_HOUSE_LOAD_DATA_SUCCESS: {
            const houseList = action.currentPage <= 2 ? action.houseList : state.houseList.concat(action.houseList);

            let footerRefreshState = FooterRefreshState.Idle;
            if (AppUtil.isEmptyArray(houseList)) {
                footerRefreshState = FooterRefreshState.EmptyData;
            } else if (!action.hasMoreData) {
                footerRefreshState = FooterRefreshState.NoMoreData;
            }

            return {
                ...state,
                houseList,
                currentPage: action.currentPage,
                headerIsRefreshing: false,
                footerRefreshState,
            }
        }
        default:
            return state;
    }
}

包装组件

class SearchHousePage extends Component {

    // ...代码省略
    componentDidMount() {
        this._loadData(true);
    }

    componentWillUnmount() {
        NavigationUtil.dispatch(Types.SEARCH_HOUSE_WILL_UNMOUNT);
    }

    _loadData(isRefresh) {
        const { loadData, searchHouse } = this.props;
        const currentPage = isRefresh ? 1 : searchHouse.currentPage;

        loadData(this.filterParams, currentPage, error => Toaster.autoDisapperShow(error));
    }
    
    render() {
        const { home, searchHouse } = this.props;

        return (
            <View style={styles.container} ref='container'>
                <RefreshFlatList
                    ref='flatList'
                    style={{ marginTop: AppUtil.fullNavigationBarHeight + 44 }}
                    showsHorizontalScrollIndicator={false}
                    data={searchHouse.houseList}
                    keyExtractor={item => `${item.id}`}
                    renderItem={({ item, index }) => this._renderHouseCell(item, index)}
                    headerIsRefreshing={searchHouse.headerIsRefreshing}
                    footerRefreshState={searchHouse.footerRefreshState}
                    onHeaderRefresh={() => this._loadData(true)}
                    onFooterRefresh={() => this._loadData(false)}
                    footerRefreshComponent={footerRefreshState => this.footerRefreshComponent(footerRefreshState, searchHouse.houseList)}
                />
                <NavigationBar
                    navBarStyle={{ position: 'absolute' }}
                    backOrCloseHandler={() => NavigationUtil.goBack()}
                    title='搜房'
                />
                <SearchFilterMenu
                    style={styles.filterMenu}
                    cityId={`${home.cityId}`}
                    subwayData={home.subwayData}
                    containerRef={this.refs.container}
                    filterMenuType={this.params.filterMenuType}
                    onChangeParameters={() => this._loadData(true)}
                    onUpdateParameters={({ nativeEvent: { filterParams } }) => {
                        this.filterParams = {
                            ...this.filterParams,
                            ...filterParams,
                        };
                    }}
                />
            </View>
        );
    }
}

const mapStateToProps = state => ({ home: state.home, searchHouse: state.searchHouse });

const mapDispatchToProps = dispatch => ({
    loadData: (params, currentPage, errorCallBack) =>
        dispatch(loadData(params, currentPage, errorCallBack)),
});

export default connect(mapStateToProps, mapDispatchToProps)(SearchHousePage);

从上面的代码使用了一个connect函数,connect连接React组件与Redux store,连接操作会返回一个新的与Redux store连接的组件类,并且连接操作不会改变原来的组件类。

mapStateToProps中订阅了home节点和searchHouse节点,该页面主要使用searchHouse节点,那订阅home节点是用来方便组件间通信,这样页面进行网络请求所需的cityId,就不需要从前以页面传入,也不需要从缓存中读取。

列表的刷新状态由headerIsRefreshingfooterRefreshState进行管理。

综上

redux已经帮我们完成了页面的状态管理,再总结一下Redux需要注意的点:

  • Redux应用只有一个单一的Store。当需要拆分数据处理逻辑时,你应该使用拆分与合并reducer而不是创建多个Store;
  • redux一个特点是:状态共享,所有的状态都放在一个Store中,任何组件都可以订阅Store中的数据,但是不建议组件订阅过多Store中的节点;
  • 不要将所有的State都适合放在Store中,这样会让Store变得非常庞大;

到这里,我们实现了列表的下拉刷新、加载更多以及如何使用redux,还差一个筛选栏和子菜单页面的开发,这里涉及到React Native与原生之间的通信,我会在React Native实现一个带筛选功能的搜房列表(3)中分享下如何进行React Native与原生的桥接开发。

另外上面提供的代码均是从项目当中截取的,如果需要查看完整代码的话,在代码传送门--NNHybrid中。

上述相关代码路径:

redux文件夹: /NNHybridRN/redux

SearchHousePage: /NNHybridRN/sections/searchHouse/SearchHousePage.js

参考资料:

Redux 中文文档

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