react-loader-factory-immutable

A factory for creating custom functional react loading screen components (immutableJS version).

Usage no npm install needed!

<script type="module">
  import reactLoaderFactoryImmutable from 'https://cdn.skypack.dev/react-loader-factory-immutable';
</script>

README

react-loader-factory-immutable

A factory for producing Redux-driven loading screens. (ImmutableJS version.)

For a version that doesn't use ImmutableJS, check out react-loader-factory.

Example

npm install --save react-loader-factory-immutable

Say you have an asynchronous request that provides data through Redux to a pure functional component. You'd like to display a loader while waiting for the request, but you don't want to pollute your beautiful pure function.

With react-loader-factory, you can do this instead:

import React from 'react';
import { myAsyncAction } from '../actions';
import loaderFactory from 'react-loader-factory-immutable';
import ChildComponent from './ChildComponent';

const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates);

const LoadingChild = loaderWrapper(ChildComponent);

const containingComponent = props => {
  // Do whatever you need to do with your usual containing component

  const childProps = { someProps: 'props' };

  return <LoadingChild { ...childProps } />;
}

You'll also need a reducer that tracks which requests are active. Something like this:

import Immutable from 'immutable';

export function activeRequests(state = Immutable.List([]), action) {
  // regex that tests for an API action string ending with _REQUEST
  const reqReg = new RegExp(/^[A-Z]+\_REQUEST$/g);
  // regex that tests for a API action string ending with _SUCCESS
  const sucReg = new RegExp(/^[A-Z]+\_SUCCESS$/g);

  // if a _REQUEST comes in, add it to the activeRequests list
  if (reqReg.test(action.type)) {
    return state.push(action.type);
  }

  // if a _SUCCESS comes in, delete its corresponding _REQUEST
  if (sucReg.test(action.type)) {
    const reqType = action.type.split('_')[0].concat('_REQUEST');
    const deleteInd = state.indexOf(reqType);

    if (deleteInd !== -1) {
      return state.delete(deleteInd);
    }
  }

  return state;
}

As long as none of the requests specified in monitoredStates have come back with a SUCCESS (or whatever you use to specify a successful request), the loader will continue to display its default throbber, or a throbber prop you pass into the returned loading component.

The guts

  1. loaderFactory(actionsList, monitoredStates) returns a higher-order component that connects to the Redux store and monitors the activeRequests state branch for values it's been told to monitor. It expects activeRequests to have a .some() method to test with. It also takes responsibility for dispatching the Redux actions specified in actionsList exactly once.

  2. If any of its monitored active requests are present, it displays this.props.throbber or a default <div> with a class this.props.throbberClass (or loader layout--flex if none is specified).

  3. If there are no more active requests the wrapped component cares about, the throbber component gets out of the way and returns the originally wrapped component, with all props passed through.

Optional state injector

You can also pass in a function as the third argument to loaderFactory that accepts state as its sole argument. The function should return an object with an array of strings called activeRequests. If any of monitoredStates are present in the array, the throbber will be rendered instead of the dependent content.

const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates, function(state) {
    return { activeRequests: state.get('customKey') };
});

Optional state dependency

If you have even more granular requirements for what the state needs to be before the loader should pass the application through, pass in a boolean function of state as the fourth argument to loaderFactory. When this function returns true and the state injector function above return true, the loader will render the wrapped component.

Why a factory?

The factory pattern is needed to set up the connect() call that hooks the component up to your Redux store. There's no way for a component to dynamically connect() itself when evaluated, so the factory pattern gives you that convenience.

Things like this