原文鏈接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è)工作流程為:
- View需要訂閱Store中的state鲜结;
- 操作View(點(diǎn)擊了View上的一個(gè)按鈕或者執(zhí)行一個(gè)網(wǎng)絡(luò)請(qǐng)求)展运,發(fā)出Action;
- 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;
- State一旦有變化纫事,Store就會(huì)調(diào)用監(jiān)聽函數(shù)勘畔,來更新View所灸;
Store
Store是存儲(chǔ)state的容器,負(fù)責(zé)提供所有的狀態(tài)炫七。整個(gè)應(yīng)用只能有一個(gè)Store爬立,這么做的目的是為了讓組件之間的通信更加簡(jiǎn)單。
在沒有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)由headerIsRefreshing
和footerRefreshState
進(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
參考資料: