原文:Where and When to Fetch Data With Redux
javascript
若是固件爲了渲染須要一些數據,你想使用Redux獲取數據,並保存在Redux Store中,那麼什麼時間點是調用API的最好時機?java
componentdidMount
生命週期函數中啓動Action假設你要顯示一個產品列表. 後臺API是:'GET/products',能夠這麼建立Redux actionreact
productAction.js
ios
export function fetchProducts() {
return dispatch => {
dispatch(fetchProductsBegin());
return fetch("/products")
.then(handleErrors)
.then(res => res.json())
.then(json => {
dispatch(fetchProductsSuccess(json.products));
return json.products;
})
.catch(error => dispatch(fetchProductsFailure(error)));
};
}
// Handle HTTP errors since fetch won't.
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
複製代碼
註釋:fetch()
不會拋出HTTP error,例如404錯誤. 這一點讓人有點困惑,若是你以前使用的是其餘的方法,例如axios
. 看這裏
,有關於fetch和錯誤處理的內容.git
在Redux中,使用redux-thunk獲取數據 一般,actions必須是一個簡單對象.返回一個函數,例如實例中的fetchProducts
,超出了範圍,Redux不容許這麼作.至少在沒有協助的狀況下不行. 因此redux-thunk
就出現了.redux-thunk
是一箇中間件能夠告訴Redux如何處理新類型的action(若是很好奇,能夠看看thunk究竟是什麼東東?
)github
等等.神馬狀況? redux-thunk,Reducers有些意義. redux-thunk是徹底捏造出來的吧?npm
若是你處在對Redux似懂非懂的邊緣,要大膽大往前嘗試,儘管可能不太明白究竟是怎麼一回事.我會把這件事說明白.json
即便你已經搞清楚了,或許理解的很透徹. 回顧一下也是值得的.redux
使用npm install redux-thunk
安裝redux-thunk.接着須要添加幾行代碼擴展Redux store,以便使用新的中間件axios
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
複製代碼
重要的一件事要注意,在傳遞給Redux以前,必需要用applyMiddleware
包裝中間件. 這裏還出現了rootReducer
,以後咱們會看看它的出處.
這段代碼能夠卸載index.js
中,或者寫在本身的文件中(store.js
是個很好的名字).Redux不關心文件放在那裏.若是你願意,放在一塊兒也能夠.只要可以獲取到store,並經過Provider
提供給app就設置完成了.
Redux action獲取數據一般是三部曲:BEGIN,SUCCESS,FAULURE,這不是必須的,只是約定俗成.
在起始API調用以前,dispatch BEGIN action api調用成功以後, dispatch SUCCESS和數據.若是api調用失敗,dispatch FAILURE和error.
有時候, 最後一次的調用用ERROR代替.不太理想,只是爲了統一.
BEGIN/SUCCESS/FAILURE 模式很好,由於他給出了一個掛鉤用於追蹤具體發生的事-例如,設置 "loading"標誌爲true
標識BEGIN action, SUCCESS或者FAILURE時設定爲false
.下面是action的樣子:
productActions.js
export const FETCH_PRODUCTS_BEGIN = 'FETCH_PRODUCTS_BEGIN';
export const FETCH_PRODUCTS_SUCCESS = 'FETCH_PRODUCTS_SUCCESS';
export const FETCH_PRODUCTS_FAILURE = 'FETCH_PRODUCTS_FAILURE';
export const fetchProductsBegin = () => ({
type: FETCH_PRODUCTS_BEGIN
});
export const fetchProductsSuccess = products => ({
type: FETCH_PRODUCTS_SUCCESS,
payload: { products }
});
export const fetchProductsFailure = error => ({
type: FETCH_PRODUCTS_FAILURE,
payload: { error }
});
複製代碼
以後,在收到FETCH_PRODUCTS_SUCCESS
action時用reducer保存products至Redux store. 也一樣在獲取數據開始時,設置loading
標誌爲true
,完成或失敗時設置爲false
.
productReducer.js
import {
FETCH_PRODUCTS_BEGIN,
FETCH_PRODUCTS_SUCCESS,
FETCH_PRODUCTS_FAILURE
} from './productActions';
const initialState = {
items: [],
loading: false,
error: null
};
export default function productReducer(state = initialState, action) {
switch(action.type) {
case FETCH_PRODUCTS_BEGIN:
// Mark the state as "loading" so we can show a spinner or something
// Also, reset any errors. We're starting fresh.
return {
...state,
loading: true,
error: null
};
case FETCH_PRODUCTS_SUCCESS:
// All done: set loading "false".
// Also, replace the items with the ones from the server
return {
...state,
loading: false,
items: action.payload.products
};
case FETCH_PRODUCTS_FAILURE:
// The request failed. It's done. So set loading to "false".
// Save the error, so we can display it somewhere.
// Since it failed, we don't have items to display anymore, so set `items` empty.
//
// This is all up to you and your app though:
// maybe you want to keep the items around!
// Do whatever seems right for your use case.
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
// ALWAYS have a default case in a reducer
return state;
}
}
複製代碼
最後只須要把products傳遞給ProductList
組件,這個組件最終顯示列表,同時也負責啓動數據獲取工做.
ProductList.js
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "/productActions";
class ProductList extends React.Component {
componentDidMount() {
this.props.dispatch(fetchProducts());
}
render() {
const { error, loading, products } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<ul> {products.map(product => <li key={product.id}>{product.name}</li> )} </ul>
);
}
}
const mapStateToProps = state => ({
products: state.products.items,
loading: state.products.loading,
error: state.products.error
});
export default connect(mapStateToProps)(ProductList);
複製代碼
這裏引用數據用了state.products.<somedata>
,沒有用state.<somedata>
, 由於我假設你可有有不止一個reducer,每一個reducer處理本身的一塊state. 爲了讓多個reducer一同工做,咱們須要rootReducer.js
文件, 它會把全部的小塊reducer組合在一塊兒:
rootReducer.js
import { combineReducers } from "redux";
import products from "./productReducer";
export default combineReducers({
products
});
複製代碼
截止,在建立store時, 能夠傳遞這個"root" reducer:
index.js
import rootReducer from './rootReducer';
// ...
const store = createStore(rootReducer);
複製代碼
這裏的錯誤處理內容不多,可是基礎的結構和執行api調用的action是同樣的. 整體的思路是:
這是一個常見的擔心.的確是會渲染超過不止一次. 在state爲空的時候渲染一次, 根據loading state會從新渲染,在顯示數據時還要渲染. 恐怖! 三次渲染!(若是直接跳過loading 會減微微兩次).
你之因此擔憂沒必要要的渲染是由於性能的考慮,可是不要擔憂,單個渲染速度很快. 若是明顯很慢,那就須要找到致使變慢的緣由.
這樣考慮:app須要在沒有內容時顯示一些東西,能夠是加載提示,或者錯誤提示. 你也不肯意在在數據到來以前顯示空白頁面.這些提示爲了咱們加強用戶體驗的機會.
從構架的觀點看,若是一個父"東東"(組件,函數,或路由)在他加載組件以前自動獲取數據就更好了. 組件本身覺察不到無心義的調用. 他們能夠幸福的等待數據.
有一些方法能夠修復這個問題,可是凡事都有得有失. 魔術加載是魔術,它們須要更多的代碼.
有不少方式能夠重構這段代碼.沒有最好的方法,由於每一個方法都有適用範圍, 由於對一個實例是最好的對於其餘的實例未必如此.
在componentDidMount
中獲取數據不是最好的一個,可是是最簡單的完成工做的方法.
若是你不喜歡這麼做,還要其餘一些方法能夠嘗試:
api
模塊中, 在action中調用(分離關注點).Dan Abramov的演示
redux-dataloader
或者redu-async-loader
.或者Mark Eriksons的數據獲取庫方法之一
.ProductListPage
.以後"Page"關注fetching,"List"僅僅接受數據並渲染他們recompose
庫把componentDidMount
周期函數放入到高階包裝函數內-這個庫是能夠工做,可是可能做者要中止了[^譯註,這個庫的做者就是React的核心成員,recompose算是react hooks的前身]