My React component needs to fetch some data A asynchronously, and then based on its contents, send a second async request to get data B. All result are stored in Redux and we use Redux-thunk.
There may be several components on the page at the same time that all need A, so there' s a good chance that it already exists n Redux; but another component could also be fetching it, and then isFetching
is true. I don't want to have multiple identical requests (not least because the browser cancels them).
Solutions for sequential actions like https://github.com/reactjs/redux/issues/1676 and https://github.com/reactjs/redux/issues/723 propose a redux-thunk that returns a promise, one that is already resolved if the object is already present; e.g.:
function getA(uuid) {
return (dispatch, getState) => {
const currentA = getState().a[uuid];
if (currentA) {
// Return resolved promise with the already existing object
return Promise.resolve(currentA);
} else {
// Return async promise
return goFetchA(uuid).then(objectA => {
dispatch(receivedA(uuid, objectA));
return objectA;
});
}
};
}
function getAthenB(uuidA, uuidB) {
return dispatch =>
dispatch(getA(uuidA)).then(
objectA => dispatch(getB(objectA, uuidB)));
}
So far, so good. But what kind of promise can I return in case the state contains both the object and an 'isFetching' boolean? This would be trivial if we could store the actual Promise of the request in the state, but that sort of thing shouldn't go into a Redux state.
function getA(uuid) {
return (dispatch, getState) => {
const currentA = getState().a[uuid];
if (currentA) {
if (!currentA.isFetching) {
return Promise.resolve(currentA.data);
} else {
// WHAT TO RETURN HERE?
}
} else {
dispatch(startFetchingA(uuid));
return goFetchA(uuid).then(objectA => {
receivedObjectA(uuid, objectA);
return objectA;
});
}
};
}
A similar problem exists when I want to cancel an ongoing request -- it's not stored anywhere, so a solution that also helps with that would be ideal.
Redux Async Data Flow Just like with a normal action, we first need to handle a user event in the application, such as a click on a button. Then, we call dispatch() , and pass in something, whether it be a plain action object, a function, or some other value that a middleware can look for.
Well, no matter what you pass to dispatch , it is still a single action. Even if your action is an array of objects, or a function which can then create more action objects!
Introduction. By default, Redux's actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects. Redux also allows for middleware that sits between an action being dispatched and the action reaching the reducers.
The action types can be organized into two groups: Direct manipulation of the store. UPDATE_RESOURCES and DELETE_RESOURCES allow you to synchronously modify the store, independent of requests. The rest of the action types are for requests.
I believe it's not possible to handle this kind of scenario without introducing some mutable, unserializable storage, which will store running requests, along with your pure, fully serializable Redux store.
There is a bunch of libraries out there which can help you with handling this kind of side-effects (Redux Saga and Redux Observable, to name a few). But if your case is limited to what you have just described, I would suggest using extra argument feature of Redux Thunk, which, I believe, is designed especially for such cases.
Fast sketch of how you can implement your task with an extra argument:
/**
* init.js
*/
const api = ...; // creating api object here
const requests = {};
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument({api, requests}))
)
/**
* actions.js
*/
function getA(uuid) {
return (dispatch, getState, {api, requests}) => {
if (requests[uuid]) return requests[uuid];
const currentA = getState().a[uuid];
if (currentA) {
return Promise.resolve(currentA.data);
}
dispatch(startFetchingA(uuid));
const request = requests[uuid] = api.fetchA(uuid);
return request.then(objectA => {
delete requests[uuid];
receivedObjectA(uuid, objectA);
return objectA;
});
};
}
Same technique can be used to cancel requests.
Also, you can probably end up with cleaner solution by introducing custom Redux middleware, but it really depends on what is your true full-blown case, and not this simplified one.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With