Creating promise actions in redux


If you're reading this you probably already know something about  Redux, and how it works with React. Just in case you don't here's a quick recap of the steps. This can seem quite complex at first, but the pattern is simple and always the same - which is one of the advantages of using Redux.
  • In your React component, you connect to the part of the Redux store you are interested in using the @connect decorator. 
  • This injects a reference to data from that store into the props of the component, along with a reference to the store dispatcher.
  • When something happens and you need to update the state in the store, you need to 
    • create an action consisting of a type, and optionally a payload. This is normally done using an action creator function that returns a plain object.
    • dispatch that action creator.
  • Next your reducer, whose job is to put things in the store, is called by Redux. That reducer will make a new copy of the state in the store, and add whatever is in the payload.

Example

When you create an action to be dispatched to Redux, that action object needs to be a plain object, and normally looks like this.
{ type:'SOMETHING_WONDERFUL', payload:magnificentPayload }

An action might look like this.
const acSomethingWonderful = (payload) => {
  return {type: "SOMETHING_WONDERFUL" , payload:payload};
}

In your React component, you would dispatch that action creator.
this.props.dispatch (acSomethingWonderful());

Asynchronicity

So far so good, but when you have to deal with asynchronous actions (such as fetching data from an API), things can get complicated, and you may be tempted to start messing with the pattern, but there's no need. 

In this example, the user has clicked the button to signout of my application, which has been authenticated using Firebase. However I can't dispatch an action saying he has signed out, because that hasn't actually happened yet. The state we're currently in is that we've asked Firebase to sign out, but it's not yet completed. This means that any React components which are being rendered based on someone being signed in now need to be updated, but not yet - not until the signout is completed, so this simple action creator will not work.

This is wrong...
const acSignout = (payload) => {
  return {type: "AUTH_SIGNOUT" , payload:payload};
}

Dispatching multiple actions

To make this right, multiple actions need to be dispatched. One to say that the signout process has started, and another when it has finished (or failed). Here's an improved version, wrapped in a function that will end up creating multiple actions. These snippets are taken from the  Ephemeral Exchange management console.
export function acSignout() {

  return acPromise (
    cs.actions.AUTH_SIGNOUT,
    getCurrentUid(), 
    () => firebase.signOut
  ); 

}
 
and in the component that detected the signout request
this.props.dispatch (acSignout());

The function to be executed (firebase.signOut), is passed to the wrapper function below, which turns it into a promise (if it's not already one). 
  • While firebase.signOut is being executed it returns an action creator of type AUTH_SIGNOUT_PENDING. 
  • You have the opportunity of catching that PENDING action in your reducer which will also receive the pendingPayload argument to put in the store. During this time all your subscribed components will get to know that there is a signout on operation, and the payload will have signalled who is signing out.
  • When the firebase.signOut is completed, it dispatches another action of type AUTH_SIGNOUT_FULFILLED
  • The reducer will action this new status so the rest of the App can get to know that the signout has happened, and that all the dependent components can be re-rendered to reflect this new state.
  • Note that the AUTH_SIGNOUT is never dispatched. Only the _PENDING and _FULFILLED (or _REJECTED) actions need to be handled in the reducer
  • Note that the function contains a reference to the store dispatcher to avoid passing it through each time. This is because I will turn this into middleware at some point and won't need the dispatcher reference then. 
/**
 * actionType_PENDING is returned from this
 * and later when the function is completed, actionType_FULFILLED or _REJECTED are dispatched
 * @param {string} actionType the base action type
 * @param {*} pendingPayload the payload to dispatch with the _PENDING action
 * @param {func} function the function to execute that should return a promise
 * @return {object} the action to be dispatched
 */
export function acPromise (actionType,  pendingPayload, func) {

  // later on I'll make this middleware so i dont need to pass the dispatcher
  // for now I have it stored somewhere
  const dispatch= Process.store.dispatch; 
  
  // first check the function is actually a promise
  // and convert it if it isnt
  const theAction = typeof func.then === 'function' ? func : function () {
    return new Promise (function (resolve, reject) {
      try {
        resolve(func());
      }
      catch(err) {
        reject (err);
      }
    });
  };
  
  // now we execute the thing, but dispatch a fullfilled/rejected when done
  theAction()
  .then (function (result) {
    // the result of the original function
    dispatch({
      type:actionType+"_FULFILLED",
      payload:result
    });
  })
  .catch (function (err) {
    dispatch({
      type:actionType+"_REJECTED",
      payload:err
    });
  });
  
  // what we return is the pending action
  return {
    type:actionType+"_PENDING",
    payload:pendingPayload
  };
  
}

Here's the Redux logger view of what happened following the dispatch.

Redux-promise middleware

There are a couple of middlewares available such as redux-promise that can  help with this, but I decided to roll my own, mainly because I wanted to be able to pass a payload while in the PENDING state too, and couldn't figure out how to do that with those other solutions. This creates the same action variants (PENDING,FULFILLED,REJECTED) as redux-promise.




For more like this, see React, redux, redis, material-UI and firebase. Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.
Comments