Async Actions

Learn how to handle async actions.

What makes middleware so powerful is the access to both getState() and dispatch(), as these functions allow a middleware to run asynchronous actions and give it full access to the store. A very simple example would be an action debounceused to save state only after a period of inactivity middleware. Suppose we have an autocomplete field, and we want to prevent the AUTO_COMPLETE action from running as the user types in a search term. We would probably want to wait 500 ms for the user to type in part of the search string and then run the query with the latest value.

We can create a debounce middleware that will catch any action with the debounce key set in its metadata and ensure it is delayed by the specified number of milliseconds. Any additional action of the same type passed before the debounce timer expires will be saved as the “latest action” rather than passed to reducers. These additional actions will be executed once the debounce timer has expired:

0ms: dispatch({ type: 'AUTO_COMPLETE', payload: 'c', meta: { debounce: 500 }};
// Suppressed

10ms: dispatch({ type: 'AUTO_COMPLETE', payload: 'ca', meta: { debounce: 500 }};
// Suppressed

20ms: dispatch({ type: 'AUTO_COMPLETE', payload: 'cat', meta: { debounce: 500 }};
// Suppressed

520ms:
// The action with payload 'cat' is dispatched by the middleware.

The skeleton of our middleware needs to inspect only actions that have the required debounce key set in their metadata:

const debounceMiddleware = () => next => action => {
  const { debounce } = action.meta || {};

  if (!debounce) {
    return next(action);
  }

  // TODO: Handle debouncing
};

Since we want each action type to have a different debounce queue, we will create a pending object that will hold information for each action type. In our case, we only need to save the latest timeout for each action type:

// Object to hold debounced actions (referenced by action.type)
const pending = {};

const debounceMiddleware = () => next => action => {
  const { debounce } = action.meta || {};

  if (!debounce) {
    return next(action);
  }

  if (pending[action.type]) {
    clearTimeout(pending[action.type])
  }

  // Save latest action object
  pending[action.type] = setTimeout(/* implement debounce */);
};

If there is already a pending action of this type, we cancel the timeout. We then create a new timeout to handle this action. The previous one can be safely ignored—for example, in our case if an action { type: 'AUTO_COMPLETE', payload: 'cat' } comes right after { type: 'AUTO_COMPLETE', payload: 'ca' }, we can safely ignore the one with 'ca' and only call the autocomplete API for 'cat':

setTimeout(
  () => {
    delete pending[action.type];
    next(action);
  },
  debounce
);

Once the timeout for the latest action has elapsed, we clear the key from our pending object and next() method. This allows the last delayed action to pass through to the other middleware and store:

// Object to hold debounced actions (referenced by action.type)
const pending = {};

const debounceMiddleware = () => next => action => {
  const { debounce } = action.meta || {};

  if (!debounce) {
    return next(action);
  }

  if (pending[action.type]) {
    clearTimeout(pending[action.type]);
  }

  pending[action.type] = setTimeout(
    () => {
      delete pending[action.type];
      next(action);
    },
    debounce
  );
};

With this basic middleware, we have created a powerful tool for our developers. A simple meta setting on an action can now support debouncing any action in the system. We have also used the middleware’s support for the next() method to selectively suppress actions.

We will learn about more advanced uses of the async flow to handle generic API requests later.

Get hands-on with 1200+ tech skills courses.