Source: app/util/redux-listeners-middleware/index.js

/**
 * @module redux-listeners-middleware
 */

class ActionListener {
  constructor({ actionType, filter }, handler) {
    this.actionTypes = [].concat(actionType || []);
    this.filters = [].concat(filter || []);
    this.handler = handler;
  }

  matchesFilter = (action, getState, dispatch) =>
    this.filters.every(filter => filter(action, getState, dispatch));
}

/**
 * This callback will be called by
 * {@link module:redux-listeners-middleware~reduxListenersMiddleware|reduxListenersMiddleware} on
 * initialization. Inside of it you can attach multiple action listeners by calling the addListener
 * function.
 *
 * @callback setupFunction
 * @param {addListenerFunction} addListener Callback that can be used to add a new action listener.
 * See {@link module:redux-listeners-middleware~addListenerFunction|addListenerFunction}
 */

/**
 * @callback addListenerFunction
 * @param {object} options
 * @param {string|Array<string>} [options.actionType] An action type or array of action type that
 * will be listened to. If not set, this listener will listen to any action type. However, for
 * performance reasons it is recommended to add action types here whenever possible.
 * @param {function|Array<function>} [options.filter] A function or array of functions that filter
 * the incoming actions. Receives the following parameters:
 *  - `action` The action object
 *  - `getState` A function that can be called to get the current Redux state
 *
 * Should return `true` if the handler function should be called.
 * @param {function} handler Handler function that will be called when an action is dispatched
 * that matches this listener.
 */

/**
 * Redux middleware that can be used to attach listeners to Redux actions.
 *
 * **IMPORTANT**: This should only be used to add additional side-effects to Redux actions, such
 * as event tracking. Using this middleware to do application logic is an anti-pattern.
 *
 * @category tracking
 * @function reduxListenersMiddleware
 * @param {setupFunction|setupFunction[]} setupFuncs An array of setup functions or a single setup
 * function that adds listeners for redux actions. See
 * {@link module:redux-listeners-middleware~setupFunction|setupFunction} for more info.
 * @returns {function} A middleware function that can be passed to Redux's
 * {@link http://redux.js.org/docs/api/applyMiddleware.html|applyMiddleware()}
 */
export default setupFuncs => {
  const listenersByActionType = {};
  const anyActionTypeListeners = [];

  function addListener(options, handler) {
    const listener = new ActionListener(options, handler);

    if (options.actionType) {
      [].concat(options.actionType).forEach(actionType => {
        if (!listenersByActionType[actionType]) {
          listenersByActionType[actionType] = [];
        }

        listenersByActionType[actionType].push(listener);
      });
    } else {
      anyActionTypeListeners.push(listener);
    }
  }

  [].concat(setupFuncs).forEach(setup => setup(addListener));

  return ({ getState, dispatch }) => next => action => {
    const actionTypeListeners = (action.type && listenersByActionType[action.type]) || [];

    anyActionTypeListeners.concat(actionTypeListeners).forEach(listener => {
      if (listener.matchesFilter(action, getState, dispatch)) {
        listener.handler(action, getState, dispatch);
      }
    });

    return next(action);
  };
};