React Native實(shí)現(xiàn)一個(gè)帶篩選功能的搜房列表(2)

原文鏈接React Native實(shí)現(xiàn)一個(gè)帶篩選功能的搜房列表(2)

上一篇中看杭,我們實(shí)現(xiàn)了一個(gè)下拉刷新和上拉加載更多的列表喳整,那根據(jù)一般的開發(fā)步驟流码,接著應(yīng)該就是進(jìn)行網(wǎng)絡(luò)請(qǐng)求提陶,在網(wǎng)絡(luò)請(qǐng)求之后更新列表數(shù)據(jù)和列表的刷新狀態(tài)行施。

這篇文章會(huì)向大家介紹一下Redux的基本概念以及在頁(yè)面中如何使用Redux進(jìn)行狀態(tài)管理允坚。

文章中的代碼都來自代碼傳送門--NNHybrid

Redux概念

首先先簡(jiǎn)單介紹一下Redux的一些概念蛾号。Redux是JavaScript狀態(tài)容器稠项,提供可預(yù)測(cè)化的狀態(tài)管理,其工作流程如下:

整個(gè)工作流程為:


reduxProcess
  1. View需要訂閱Store中的state鲜结;
  2. 操作View(點(diǎn)擊了View上的一個(gè)按鈕或者執(zhí)行一個(gè)網(wǎng)絡(luò)請(qǐng)求)展运,發(fā)出Action;
  3. Store自動(dòng)調(diào)用Reducer精刷,并且傳入兩個(gè)參數(shù)(Old State和Action)拗胜,Reducer會(huì)返回新的State,如果有Middleware怒允,Store會(huì)將Old State和Action傳遞給Middleware埂软,Middleware會(huì)調(diào)用Reducer 然后返回新的State;
  4. State一旦有變化纫事,Store就會(huì)調(diào)用監(jiān)聽函數(shù)勘畔,來更新View所灸;

Store

Store是存儲(chǔ)state的容器,負(fù)責(zé)提供所有的狀態(tài)炫七。整個(gè)應(yīng)用只能有一個(gè)Store爬立,這么做的目的是為了讓組件之間的通信更加簡(jiǎn)單。

reduxCommunication

在沒有Store的情況下诉字,組件之間需要通信就比較麻煩懦尝,如果一個(gè)父組件要將狀態(tài)傳遞到子組件,就需要通過props一層一層往下傳壤圃,一個(gè)子組件的狀態(tài)發(fā)生改變并且要讓父組件知道陵霉,則必須暴露一個(gè)事件出去才能通信。這就使得組件之間通信依賴于組件的層次結(jié)構(gòu)伍绳。此時(shí)如果有兩個(gè)平級(jí)的節(jié)點(diǎn)想要通信踊挠,就需要通過它們的父組件進(jìn)行中轉(zhuǎn)。
有了這個(gè)全局的Store之后冲杀,所有的組件變成了和Store進(jìn)行通信效床。這樣組件之間通信就會(huì)變少,當(dāng)Store發(fā)生變化权谁,對(duì)應(yīng)的組件也能拿到相關(guān)的數(shù)據(jù)剩檀。當(dāng)組件內(nèi)部有時(shí)間觸發(fā)Store的變化時(shí),更新Store即可旺芽。這也就是所謂的單向數(shù)據(jù)流過程沪猴。

Store的職責(zé)如下:

  • 維持應(yīng)用的state;
  • 提供getState()方法獲取state采章;
  • 提供dispatch(action)方法更新state运嗜;
  • 通過subscribe(listener)注冊(cè)監(jiān)聽器;
  • 通過subscribe(listener)返回的函數(shù)注銷監(jiān)聽器悯舟。

Action

當(dāng)我們想要更改store中的state時(shí)担租,我們便需要使用Action。Action是Store數(shù)據(jù)的唯一來源抵怎,每一次修改state便要發(fā)起一次Action奋救。

Action可以理解為是一個(gè)Javascript對(duì)象。其內(nèi)部必須包含一個(gè)type字段來表示將要執(zhí)行的動(dòng)作反惕,除了 type字段外菠镇,Action的結(jié)構(gòu)完全由自己決定。多數(shù)情況下承璃,type字段會(huì)被定義成字符串常量。

Action舉例:

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

Action創(chuàng)建函數(shù)

Action創(chuàng)建函數(shù)就是生成action的方法蚌本】猓“action” 和 “action 創(chuàng)建函數(shù)” 這兩個(gè)概念很容易混在一起隘梨,使用時(shí)最好注意區(qū)分。

Action創(chuàng)建函數(shù)舉例:

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

Reducer

Store收到Action以后舷嗡,必須給出一個(gè)新的State轴猎,這樣View才會(huì)發(fā)生變化。
這種State的計(jì)算過程就叫做Reducer进萄。Reducer是一個(gè)純函數(shù)捻脖,它只接受Action和當(dāng)前State作為參數(shù),返回一個(gè)新的State中鼠。

由于Reducer是一個(gè)純函數(shù)可婶,所以我們不能在reducer里執(zhí)行以下操作:

  • 修改傳入的參數(shù);
  • 執(zhí)行有副作用的操作援雇;
  • 調(diào)用非純函數(shù)矛渴;
  • 不要修改state;
  • 遇到未知的action時(shí)惫搏,一定要返回舊的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

在開發(fā)過程中,由于有的功能是相互獨(dú)立的筐赔,所以我們需要拆分reducer铣猩。一般情況下,針對(duì)一個(gè)頁(yè)面可以設(shè)置一個(gè)reducer茴丰。但redux原則是只允許一個(gè)根reducer达皿,接下來我們需要將每個(gè)頁(yè)面的的reducer聚合到一個(gè)根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創(chuàng)建函數(shù)

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);
    }
}

創(chuàng)建reducer

// 默認(rèn)的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);

從上面的代碼使用了一個(gè)connect函數(shù)较沪,connect連接React組件與Redux store鳞绕,連接操作會(huì)返回一個(gè)新的與Redux store連接的組件類,并且連接操作不會(huì)改變?cè)瓉淼慕M件類尸曼。

mapStateToProps中訂閱了home節(jié)點(diǎn)和searchHouse節(jié)點(diǎn)们何,該頁(yè)面主要使用searchHouse節(jié)點(diǎn),那訂閱home節(jié)點(diǎn)是用來方便組件間通信控轿,這樣頁(yè)面進(jìn)行網(wǎng)絡(luò)請(qǐng)求所需的cityId冤竹,就不需要從前以頁(yè)面?zhèn)魅耄膊恍枰獜木彺嬷凶x取茬射。

列表的刷新狀態(tài)由headerIsRefreshingfooterRefreshState進(jìn)行管理鹦蠕。

綜上

redux已經(jīng)幫我們完成了頁(yè)面的狀態(tài)管理,再總結(jié)一下Redux需要注意的點(diǎn):

  • Redux應(yīng)用只有一個(gè)單一的Store在抛。當(dāng)需要拆分?jǐn)?shù)據(jù)處理邏輯時(shí)钟病,你應(yīng)該使用拆分與合并reducer而不是創(chuàng)建多個(gè)Store;
  • redux一個(gè)特點(diǎn)是:狀態(tài)共享,所有的狀態(tài)都放在一個(gè)Store中肠阱,任何組件都可以訂閱Store中的數(shù)據(jù)票唆,但是不建議組件訂閱過多Store中的節(jié)點(diǎn);
  • 不要將所有的State都適合放在Store中屹徘,這樣會(huì)讓Store變得非常龐大走趋;

到這里,我們實(shí)現(xiàn)了列表的下拉刷新噪伊、加載更多以及如何使用redux簿煌,還差一個(gè)篩選欄和子菜單頁(yè)面的開發(fā),這里涉及到React Native與原生之間的通信鉴吹,我會(huì)在React Native實(shí)現(xiàn)一個(gè)帶篩選功能的搜房列表(3)中分享下如何進(jìn)行React Native與原生的橋接開發(fā)姨伟。

另外上面提供的代碼均是從項(xiàng)目當(dāng)中截取的,如果需要查看完整代碼的話拙寡,在代碼傳送門--NNHybrid中授滓。

上述相關(guān)代碼路徑:

redux文件夾: /NNHybridRN/redux

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

參考資料:

Redux 中文文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肆糕,隨后出現(xiàn)的幾起案子般堆,更是在濱河造成了極大的恐慌,老刑警劉巖诚啃,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淮摔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡始赎,警方通過查閱死者的電腦和手機(jī)和橙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來造垛,“玉大人魔招,你說我怎么就攤上這事∥辶桑” “怎么了办斑?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)杆逗。 經(jīng)常有香客問我乡翅,道長(zhǎng),這世上最難降的妖魔是什么罪郊? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任蠕蚜,我火速辦了婚禮,結(jié)果婚禮上悔橄,老公的妹妹穿的比我還像新娘靶累。我一直安慰自己腺毫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布挣柬。 她就那樣靜靜地躺著拴曲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凛忿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天竞川,我揣著相機(jī)與錄音店溢,去河邊找鬼。 笑死委乌,一個(gè)胖子當(dāng)著我的面吹牛床牧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播遭贸,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼戈咳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼积仗!你這毒婦竟也來了蝇率?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤桦山,失蹤者是張志新(化名)和其女友劉穎耳贬,沒想到半個(gè)月后踏堡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咒劲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年顷蟆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腐魂。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡帐偎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛔屹,到底是詐尸還是另有隱情削樊,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布判导,位于F島的核電站嫉父,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏眼刃。R本人自食惡果不足惜绕辖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望擂红。 院中可真熱鬧仪际,春花似錦围小、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至成榜,卻和暖如春框舔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赎婚。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工刘绣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人挣输。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓纬凤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親撩嚼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子停士,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344