5

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.

4
  • In getA , in the thunk function (dispatch, getState) => { ... }, why are non-actions being returned? It seems like you should instead call dispatch with the promise/objectA in an action object. Commented Nov 20, 2017 at 15:24
  • @bsapaka: the returned value of the thunk function is returned by dispatch(); I return the promise that it can be used in getAthenB. As far as I know actions (and the Redux state) are supposed to be plain strings, objects etc, not promises? Commented Nov 20, 2017 at 15:32
  • I got the idea to return those promises from the examples I linked to. Commented Nov 20, 2017 at 15:32
  • I am using a workaround now -- I have an outer component that makes makes sure all the required As exist in the Redux state, and when they do it renders an inner component that can assume the As are present and fetches the missing Bs. That works, but I'm still wondering if there isn't something better. Commented Nov 20, 2017 at 15:33

1 Answer 1

4
+100

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, food for thought!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.