原文: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的前身]