Skip to content

Instantly share code, notes, and snippets.

@jasonrhodes
Last active October 13, 2017 02:57
Show Gist options
  • Save jasonrhodes/af4e825103593a863ed529f66b7461f2 to your computer and use it in GitHub Desktop.
Save jasonrhodes/af4e825103593a863ed529f66b7461f2 to your computer and use it in GitHub Desktop.
the trouble with redux async chaining
import asyncDispatcher from './asyncDispatcher'
export function updateThing1(options) {
return asyncDispatcher('UPDATE_THING_1', options);
}
export function updateThing2(options) {
return asyncDispatcher('UPDATE_THING_2', options);
}
export function updateSequence(thing_1_options) {
return (dispatch) => dispatch(updateThing1(thing_1_options))
.then((results) => dispatch(updateThing2(results))
}
export default function asyncDispatcher(baseType, options) {
return (dispatch) => {
dispatch({ type: `${baseType}_PENDING` })
return doAsyncThing(options)
.then((results) => {
dispatch({ type: `${baseType}_SUCCESS`, payload: results })
})
.catch((err) => {
dispatch({ type: `${baseType}_FAIL`, payload: err })
// QUESTION: TO THROW OR NOT TO THROW?
// throw err
})
}
}
class ChainingAsync extends React.Component {
componentWillReceiveProps(newProps) {
// handle changes in connected props to do things
// like show alerts, redirect, etc.
}
updateThings(values) {
this.props.updateSequence(values)
// ^^ HERE, ASSUME updateThing1 FAILED...
// if the asyncDispatcher DOES NOT throw on fail:
// we would've went on to updateThing2 with an undefined results value
// because we swallowed the error from updateThing1
// if the asyncDispatcher DOES throw on fail:
// we now HAVE to add a .catch() here, for no real reason, like so:
.catch(() => {
// do nothing
})
// otherwise we get Unhandled promise rejection errors anytime
// any of those async actions fail
}
}
@jasonrhodes
Copy link
Author

jasonrhodes commented Oct 13, 2017

A possible solution?

asyncDispatcher becomes:

export default function asyncDispatcher({ type, meta, chainable }) => (dispatch, getState, { chainable } = {}) => {
  const types = createTypes(type)
  dispatch({ type: types.PENDING })

  return doAsyncThing(options)
    .then((results) => {
      dispatch({ type: types.SUCCESS, payload: results })
    })
    .catch((err) => {
      dispatch({ type: types.FAIL, payload: err }) // not sure if i want to dispatch if chainable is true or _just_ throw hmm
      if (chainable) {
        throw err
      }
    })
  }
}

Then a 'series' helper:

export function series({ type, actions }) {
  const types = createTypes(type) // makes `${type}_PENDING`, etc
  return (dispatch, getState) => {
    dispatch({ type: types.PENDING })
    return actions.reduce(
      (promise, then) => promise.then((results) => {
        const thunk = then(results)
        return thunk(dispatch, getState, { chainable: true }) // eek semi-hacking redux-thunk
      }),
      Promise.resolve()
    )
    .then((results) => dispatch({ type: types.SUCCESS, payload: results }))
    .catch((err) => dispatch({ type: types.FAIL, payload: err }))
}

Then the actions

import asyncDispatcher from './asyncDispatcher'

export function updateThing1(options) {
  return asyncDispatcher({ type: 'UPDATE_THING_1', meta: options });
}

export function updateThing2(options) {
  return asyncDispatcher({ type: 'UPDATE_THING_2', meta: options });
}

export function updateSequence(thing_1_options) {
  return series({
    type: 'UPDATE_THINGS_SEQUENCE',
    actions: [
      () => updateThing1(thing_1_options),
      (results) => updateThing2(results)
    ]
  })
}

Now if updateThing1 fails, you'd have two actions:

UPDATE_THING_1_FAIL
UPDATE_THINGS_SEQUENCE_FAIL

No .catch() needed anywhere

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment