Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sequential async actions in Redux and isFetching

Tags:

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.

like image 450
RemcoGerlich Avatar asked Nov 15 '17 08:11

RemcoGerlich


People also ask

How can async actions handled in Redux?

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.

Can you dispatch two actions in Redux?

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!

Is Redux store dispatch synchronous?

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.

What are actions types in Redux?

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.


1 Answers

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.

like image 158
fkulikov Avatar answered Oct 11 '22 13:10

fkulikov