Redux: Aborting Fetches in Middleware

Created on 29 Feb 2016  路  5Comments  路  Source: reduxjs/redux

Hi all, this isn't a bug but I would appreciate some feedback.

I have a middleware that runs fetches for me, essentially I can dispatch an action like the following and it will run the fetch for me and then invoke the relevant callback.

{
  type: 'FETCH_USER'
  meta: {
    method: 'GET'
    endpoint: 'http://api.mydomain.com/user/1,
    success: successCallbackFunction // (possibly an action creator to be dispatched).
    error: errorCallbackFunction // (possibly an action creator to be dispatched).
  }

Typically this sort of action is dispatched in a componentWillMount method to do an async fetch of the data the component requires.

This has been working well for me but recently I noticed a couple of bugs relating to this approach. If the order of execution is as follows:

  1. A component is mounted and dispatches an action like the one above.
  2. The fetch middleware begins the fetch.
  3. The user changes page causing the requesting component (in step 1) to be unmounted, this component, or some other higher up the tree component that is also being unmounted dispatches an action to remove various parts of the store.
  4. The fetch completes triggering a reducer that puts data back in the store that should no longer be there due to step 3. And potentially this fetch was a subset of the data required. I.e. data is added to my store that is no longer relevant.

The react documentation here states:

When fetching data asynchronously, use componentWillUnmount to cancel any outstanding requests before the component is unmounted.

As far as I am aware fetch has no abort() method like the jQuery ajax does as used in the example on the React documentation. On top of that, because the fetch is handled by my middleware I don't really have a reference to it in my component.

To mitigate the issue I have put logic in the reducers for the callbacks to only mutate the state if relevant conditions are true. This works well but it feels like unnecessary callbacks, reducers and fetches are happening and I worry about how this would be as the app grows.

Does anyone have any feedback or tips on this kind of approach and how you deal with cleaning up when a component unmounts.

Thanks!

question

Most helpful comment

There currently is no cancellation support in fetch() API although that may change in the future.

If you change your middleware to use XMLHttpRequest directly you can tweak it to return the { abort } method.

const myMiddleware = store => next => action => {
  const xhr = // ...
  const ready = new Promise((resolve, reject) => {
    xhr.onload = () => { dispatch(...); resolve() }
    xhr.onerror = () => { dispatch(...); reject(...) }
    // ...
    xhr.send()
  })
  const abort = xhr.abort.bind(xhr)
  return {  ready, abort }
}

Then your components will have access to it as a part of dispatch() return value:

const operation = this.props.dispatch(fetchStuff())
operation.ready.then(...)
operation.abort()

All 5 comments

There currently is no cancellation support in fetch() API although that may change in the future.

If you change your middleware to use XMLHttpRequest directly you can tweak it to return the { abort } method.

const myMiddleware = store => next => action => {
  const xhr = // ...
  const ready = new Promise((resolve, reject) => {
    xhr.onload = () => { dispatch(...); resolve() }
    xhr.onerror = () => { dispatch(...); reject(...) }
    // ...
    xhr.send()
  })
  const abort = xhr.abort.bind(xhr)
  return {  ready, abort }
}

Then your components will have access to it as a part of dispatch() return value:

const operation = this.props.dispatch(fetchStuff())
operation.ready.then(...)
operation.abort()

Awesome. Thanks @gaearon I'll give that try. I didn't actually realise that middleware could return and that would then be the return value of dispatch()!

Yep, this should work as long as any middleware in chain before it takes care to return next return value. Which well-behaved middleware should do.

@gaearon , thanks for the answer above. I'm still wrapping my head around writing custom middleware, and I had a quick question based on this comment:

Yep, this should work as long as any middleware in chain before it takes care to return next return value. Which well-behaved middleware should do.

You state that well-behaved middleware should return the value of next, yet the example you gave returns { ready, abort }. Since this isn't returning next, is this middleware still considered well-behaved?

My interpretation of this is that the xhr middleware you wrote above needs to be listed last in the middleware stack. Is that right, or am I misunderstanding something?


Update: I think I'm slowly getting a better understanding of this. Middleware doesn't seem to necessarily have to return next if it's handling the action itself. It seems next is used to forward along the action to the next middleware if the current middleware won't be handling it, or something. Still figuring it out tho' 馃挱

Keep in mind that you use 'next()' in your middleware simply to bypass the invocation of the current middleware again during this flow. If you had always used the same 'dispatch', you would end up in the same middleware again.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dmitry-zaets picture dmitry-zaets  路  3Comments

ramakay picture ramakay  路  3Comments

jimbolla picture jimbolla  路  3Comments

amorphius picture amorphius  路  3Comments

mickeyreiss-visor picture mickeyreiss-visor  路  3Comments