Loads async data for Redux apps focusing on preventing duplicated requests and dealing with async dependencies.
Deeply inspired by alt Data Souces API, also inspired by redux-saga.
Instead of using redux-thunk, it handles wrapped actions and sideload async data. It also caches data requests for a while in order to prevent duplicated requests.
npm install redux-dataloader --save
import { load } from 'redux-dataloader'; export const FETCH_USER_REQUEST = 'myapp/user/FETCH_USER/REQUEST'; export const FETCH_USER_SUCCESS = 'myapp/user/FETCH_USER/SUCCESS'; export const FETCH_USER_FAILURE = 'myapp/user/FETCH_USER/FAILURE'; export function fetchUserRequest(username) { // use `load` to wrap a request action, load() returns a Promise return load({ type: FETCH_USER_REQUEST, payload: { username: username, }, }) } export function fetchUserSuccess(username, data) { // ... } export function fetchUserFailure(username, error) { // ... }
import { createLoader, fixedWait } from 'redux-dataloader'; import * as userActions from './userActions'; const userLoader = createLoader(userActions.FETCH_USER_REQUEST, { /* * (required) Handle fetched data, return a success action */ success: (context, result) => { // you can get original request action from context const action = context.action; const username = action.payload.username; return userActions.fetchUserSuccess(username, result); }, /* * (required) Handle error, return a failure action */ error: (context, error) => { const action = context.action; const username = action.payload.username; return userActions.fetchUserFailure(username, error); }, /* * (optional) By default, original request action will be dispatched. But you can still modify this process. */ // loading: ({ action }) => {} /* * (required) Fetch data. * We use yahoo/fetchr as an example. */ fetch: (context) => { const action = context.action; const username = action.payload.username; const fetchr = context.fetchr; return fetchr.read('userService') .params({ username, }).end(); }, /* * (optional) !!! Different from alt API. * When shouldFetch returns false, it will prevent fetching data. */ shouldFetch: (context) => { const action = context.action; const username = action.payload.username; const getState = context.getState; return !getState().user.users[username]; } }, { ttl: 10000, retryTimes: 3, retryWait: fixedWait(500), }); export default [userLoader];
import { createStore, applyMiddleware } from 'redux'; import { createDataLoaderMiddleware } from `redux-dataloader`; import { Fetchr } from 'fetchr'; import reducer from './reducers'; import loaders from './dataloaders'; const fetcher = new Fetcher({ xhrPath: '/api', }); // create middleware, you can add extra arguments to data loader context const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, { fetchr }); const store = createStore( reducer, applyMiddleware(dataLoaderMiddleware) ) // ...
Then, just use it in your application. The following is an example that combined with redial for isomorphic use.
import { provideHooks } from 'redial'; import { fetchUserRequest } from 'userActions'; import { fetchArticleRequest } from 'articleAction'; import { fetchArticleSkinRequest } from 'articleSkinAction'; // the router location is: /:username/:articleId // Data dependency: user <= article <= articleSkin async function fetchData({param, dispatch, getState}) { try { // 1. Fetch user const username = params.username; await dispatch(fetchUserRequest(username)); // wait for response // 2. Fetch article by userId and articleId, you may use useId for authentication const user = getState().user.users[username]; if (!user) { throw new Error(`user_not_found: ${username}`); } const articleId = params.articleId; await dispatch(fetchArticleRequest(user.id, articleId)); // 3. Fetch article skin by articleId const article = getState().article.articles[articleId]; if (!article) { throw new Error(`article_not_found: ${articleId}`); } await dispatch(fetchArticleSkinRequest(article.skinId)); } catch (err) { // ... } } function mapStateToProps(state, owndProps) { // ... } @connect(mapStateToProps) @provideHooks({ fetch: fetchData, }) export default class ArticleContainer extends React.Component { // ... }
You can also write fetchData() with Promise:
function fetchData({param, dispatch, getState}) { return Promise.resolve().then(() => { // 1. Fetch user const username = params.username; return dispatch(fetchUserRequest(username)); }).then(() => { // 2. Fetch article by userId and articleId, you may use useId for authentication const user = getState().user.users[username]; if (!user) { throw new Error(`user_not_found: ${username}`); } const articleId = params.articleId; return dispatch(fetchArticleRequest(user.id, articleId)); }).then(() => { // 3. Fetch article skin by articleId const article = getState().article.articles[articleId]; if (!article) { throw new Error(`article_not_found: ${articleId}`); } return dispatch(fetchArticleSkinRequest(article.skinId)); }).catch((err) => { // error handler // ... }) }
MIT