Skip to content

Instantly share code, notes, and snippets.

@caub
Last active March 22, 2018 18:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save caub/556a04f4a2a1d0af326e99b593916aa2 to your computer and use it in GitHub Desktop.
Save caub/556a04f4a2a1d0af326e99b593916aa2 to your computer and use it in GitHub Desktop.
/**
This middleware enhances redux-thunk, to deal with asynchronous actions
conventions and usage:
- an action is a plain object `{type: TYPE_CONSTANT, value: ANY_VALUE}` or an array of those actions (handled by reducer)
- you can shape action creators as `queryArgs => promise` (where the promise returns an action)
- or `queryArgs => action` (where the action value is a promise).
examples:
const getProfile = id => ({ type: GET_PROFILE, value: fetchApi(`/profile/${id}`) });
const getProfile = id => fetchApi(`/profile/${id}`).then(value => ({ type: GET_PROFILE, value }));
*/
export default function promiseMiddleware() {
return next => async action => {
if (typeof action.then === 'function') {
return Promise.resolve(action)
.then(next)
.catch(e => {
// handleErr(e.message, next);
throw e;
});
} else if (action.value && typeof action.value.then === 'function') {
return Promise.resolve(action.value)
.then(value => {
next({ type: action.type, value });
})
.catch(e => {
// handleErr(e.message, next);
throw e;
});
}
// else, not a promise
try {
return next(action);
} catch (e) {
console.error(e);
// handleErr(e.message, next);
}
};
}

Concurrent actions

const fetchCourse = (id, args) => fetchApi(`${courseEndpoint}/id`, args);
const fetchMetadata = args => fetchApi(metadataEndpoint, args);

const getAllCourseData = ({ id, tags }) => ({
  type: SET_DATA,
  value: Promise.all([fetchCourse(id), fetchMetadata(tags)])
    .then(([coursesData, tagsData]) => ({ courses: coursesData, metadata: tagsData }))
})

// or if you want to dispatch 2 different actions:
const getAllCourseData = ({ id, tags }) => Promise.all([
  fetchCourse(id).then(data =>({ type: SET_COURSE, value: data.course })),
  fetchMetadata(tags).then(data => ({ type: SET_METADATA, value: data.tags })),
]);

// which is more or less equivalent to:
const getAllCourseData = ({ id, tags }) => dispatch => Promise.all([
  fetchCourse(id).then(data => dispatch({ type: SET_COURSE, value: data.course })),
  fetchMetadata(tags).then(data => dispatch({ type: SET_METADATA, value: data.tags })),
]); // except this one dispatch those actions as early as possible

// for all those cases, it will error if one of the sub-promise errors

Cancel previous actions and use latest

let controller;

const myTask = args => dispatch => {
  if (controller) { // if a previous one exist, cancel it
    controller.abort();
  }
  controller = new AbortController();
  return fetchApi(endpoint, { ...args, signal: controller.signal })
    .then(data => dispatch({ type: MY_ACTION, value: data }));
};
// you could make any promise-based function abortable, not just this fetch wrapper

Sequential actions

const playLevelOne = () => value /*score or promise returning score*/;

const playLevelTwo = () => value /*score or promise returning score*/;

const playLevelThree = () => value /*score or promise returning score*/;

const game = () => dispatch => [playLevelOne, playLevelTwo, playLevelThree]
  .reduce(async (_, playLevel) => {
    await p;
    const value = await playLevel();
    dispatch({ type: SET_SCORE, value });
  }, Promise.resolve());

// or without await
const game = () => dispatch => [playLevelOne, playLevelTwo, playLevelThree]
  .reduce((p, playLevel) => {
    return p.then(playLevel).then(value => dispatch({ type: SET_SCORE, value }));
  }, Promise.resolve());

// both stop and throw the error as soon as an action throws
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment