React Native實現一個帶篩選功能的搜房列表(2)

原文連接React Native實現一個帶篩選功能的搜房列表(2)git

上一篇中,咱們實現了一個下拉刷新和上拉加載更多的列表,那根據通常的開發步驟,接着應該就是進行網絡請求,在網絡請求以後更新列表數據和列表的刷新狀態。github

這篇文章會向你們介紹一下Redux的基本概念以及在頁面中如何使用Redux進行狀態管理。redux

文章中的代碼都來自代碼傳送門--NNHybrid。開始以前,咱們先看一下最終實現的效果 api

search_house

Redux概念

首先先簡單介紹一下Redux的一些概念。Redux是JavaScript狀態容器,提供可預測化的狀態管理,其工做流程以下:緩存

整個工做流程爲: bash

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便可。這也就是所謂的單向數據流過程。app

Store的職責以下:函數

  • 維持應用的state;
  • 提供getState()方法獲取state;
  • 提供dispatch(action)方法更新state;
  • 經過subscribe(listener)註冊監聽器;
  • 經過subscribe(listener)返回的函數註銷監聽器。

Action

當咱們想要更改store中的state時,咱們便須要使用Action。Action是Store數據的惟一來源,每一次修改state便要發起一次Action。ui

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 中文文檔

相關文章
相關標籤/搜索