Skip to content
Tyler Shaddix edited this page Mar 30, 2016 · 18 revisions

Async Actions

There is often the need to have complex or asynchronous action handling in apps. This package splits action generation and action handling into two separate pieces of the application (UI Components and the background page), making async actions a little tricky. The alias middleware aims to solve this complexity.

alias

alias is a simple middleware which can map actions to new actions. This is useful for asynchronous actions, where the async action handling needs to take place in the background page, but it kicked off by an action from a UI Component.

For example, let's say you want to get the current session in your UI components when your component mounts. Let's start by firing off a simple action GET_SESSION.

// popover/App.jsx

import React, {Component} from 'react';
import {connect} from 'react-redux';

// the mock action
const getSession = () => {
  const data = {
    type: GET_SESSION,
    payload: {}
  };

  return data;
};

class App extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    this.props.dispatch(getSession());
  }

  render() {
    return (
      <div>
        {this.props.session && this.props.users[this.props.session.userId].name}
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    session: state.session,
    users: state.users
  };
};

export default connect(mapStateToProps)(App);

All of our session logic lives in our background page (such as fetching from local storage or making a web request). If we leave this as is, our Redux store would get an action of {type: GET_SESSION}, which is not very useful to our reducers considering we don't have any session data yet.

Using an alias, we can intercept the original GET_SESSION and replace it with a new action that we generate on the background page. We start by writing the intercept function, which will receive the original action and produce a new action, and then export it under the name of the action we will intercept:

// aliases.js

const getSession = (orginalAction) => {
  // return a thunk/promise/etc
};

export default {
  'GET_SESSION': getSession // the action to proxy and the new action to call
};

Then you just include it in your middleware for Redux:

import {alias} from 'react-chrome-redux';

import aliases from '../aliases';

const middleware = [
  alias(aliases),
  // whatever middleware you want (like redux-thunk)
];

// createStoreWithMiddleware... you know the drill

Now, when your Redux store sees an action of GET_SESSION, it will run our getSession() alias in our aliases file and return the result as the new action.

The nice thing about doing this through a middleware is that you can run it with other packages such as redux-thunk. Your alias can return a function to run instead of an action object, and everything will proceed as normal.

Action Responses

It's a common practice to have Redux dispatches return promises with something like redux-thunk. This package provides a way to simulate this behavior by providing a dispatchResponder in the store wrapper for the background page.

dispatchResponder

When an action is dispatched from a UI Component, it will return a promise that will resolve/reject with a response from the background page:

// the mock action
const getSession = () => {
  const data = {
    type: ACTION_GET_SESSION,
    payload: {}
  };

  return data;
};

class App extends Component {
  constructor(props) {}

  componentDidMount() {
    // promise returned from `dispatch()`
    this.props.dispatch(getSession())
      .then((data) => {
        // the response data
      })
      .catch((err) => {
        // something broke in the background store
      });
  }

  render() {}
}

This is really nice for making UI updates for error/success messages or pending states. By default, this is done by calling a simple Promise.resolve() on the dispatch result in the background and responding with an object of {error, value} based on rejection or resolution.

What if you don't return promises from your dispatches? Or what if those promises are hidden away in somewhere in the payload? You can pass in a dispatchResponder to your store wrapper when calling wrapStore to take care of that:

// redux-promise-middleware returns promises under `promise` field in the payload

/**
 * Respond to action based on `redux-promise-middleware` result
 * @param  {object} dispatchResult The resulting object from `store.dispatch()`
 * @param  {func}   send           func to be called when sending response. Should be in form {value, error}
 */
const reduxPromiseResponder = (dispatchResult, send) => {
  Promise
    .resolve(dispatchResult.payload.promise) // pull out the promise
    .then((res) => {
      // if success then respond with value
      send({
        error: null,
        value: res
      });
    })
    .catch((err) => {
      // if error then respond with error
      send({
        error: err,
        value: null
      });
    });
};

// ...

// Add responder to store
wrapStore(store, {
  portName: 'MY_APP',
  dispatchResponder: reduxPromiseResponder
});

Initializing UI Components

By default, the Proxy Store state is an empty object ({}). This can lead to issues if you are expecting your UI Component to have the same state as your background page on load. The load process is:

Initialize Proxy Store ===> Create long-lived connection ===> Receive background state ===> Proxy Store now in-sync (same state as background)

It's possible to render your application after this process is complete (ensuring the Proxy Store is in-sync with the background store). In #5, nhardy suggests the following pattern in UI Components:

import {Store} from 'react-chrome-redux';

const store = new Store({
  portName: 'MY_APP'
});

const unsubscribe = store.subscribe(() => {
   unsubscribe(); // make sure to only fire once
   render(
    <Provider store={store}>
      <App/>
    </Provider>
    , document.getElementById('app'));
});

This pattern will listen for the first state change event (when the background and Proxy Store are synced) before rendering the application.


Want to get into the nitty gritty? Head over to the API docs.

Clone this wiki locally