Using a message bus and React context instead of Redux with promise based APIs
Tags: react
Published: 2020-05-20 23:03:35

## Why do you want to do this?

Setting up a React context is easy — there are plenty of resources showing you how but the basics are to hook in somewhere in your call stack:

    function Main (props) {
      return (
        <div>
          <AccountProvider>
              ...
                            <VersionsProvider>
                                <NotificationsProvider>
                                  ...
                                                <AppWithAuth/>

and then, in our case, define a reducer for use in the context:

    const VersionsContext = React.createContext(EMPTY_STATE);
    
    function VersionsProvider(props) {
      const { children } = props;
      const [state, dispatch] = useReducer(reducer, EMPTY_STATE);
      const [isInitialization, setIsInitialization] = useState(true);
    
      useEffect(() => {
        if (isInitialization) {
          beginListening(dispatch);
          const lfg = new LocalForageHelper(VERSIONS_CONTEXT_NAMESPACE);
          lfg.getState()
            .then((diskState) => {
              const myDiskState = diskState || MY_STORED_EMPTY_STATE;
              dispatch(initializeVersionsAction(myDiskState));
              setIsInitialization(false);
            });
        }
        return () => {
        };
      }, [isInitialization]);
    
      return (
        <VersionsContext.Provider value={[state, dispatch]}>
          {children}
        </VersionsContext.Provider>
      );
    }
    
    export { VersionsContext, VersionsProvider };

Now you can use your contexts anywhere you like:

    const [investiblesState, investiblesDispatch] = useContext(InvestiblesContext);
    const [, versionsDispatch] = useContext(VersionsContext);

so long as you are inside a function where React hooks are viable. Setting up a message bus is similarly simple — in this case [Uclusion](https://www.uclusion.com/?utm_source=devto&utm_medium=blog&utm_campaign=devsansredux) uses the Amplify library:

    function beginListening (dispatch) {
      registerListener(VERSIONS_HUB_CHANNEL, 'versionVersionStart', (data) => {
        const { payload: { event, globalVersion, notificationVersion, marketId } } = data;
        switch (event) {
    ...
          case NOTIFICATION_MESSAGE_EVENT:
            return getNotifications()
              .then((notifications) => {
                const notification = notifications.find((item) => item.type_object_id.startsWith('notification'));
              dispatch(refreshNotificationVersionAction(notification));
              });
          case NOTIFICATION_VERSION_UPDATE:
            dispatch(updateNotificationVersion(notificationVersion));
            break;
          default:
        }
      });
    }

where

    import { Hub } from '@aws-amplify/core';
    const busListeners = {};

    /* Adds a listener to under the UNIQUE name, to the channel
     * If a listener with the name already exists, it will be removed
     * before this one is added
     */
    export function registerListener(channel, name, callback) {
      const previousListener = busListeners[name];
      if (!!previousListener) {
        Hub.remove(channel, previousListener);
      }
      busListeners[name] = callback;
      Hub.listen(channel, busListeners[name]);
    }

Setting up and using [redux-thunk](https://github.com/gaearon/redux-thunk) or [redux-promise](https://github.com/acdlite/redux-promise) so that Redux can work with Promises is IMHO much harder and not so readily in many developer’s wheelhouse.

## What can I do once I have that setup?

Once you have the above setup the pattern is very simple; when a Promise resolves send a message to one of your listeners and they will in turn dispatch to their reducer.

For instance let’s say I have a simple reducer:

    function computeNewState (state, action) {
      switch (action.type) {
        case UPDATE_MESSAGES:
          return doUpdateMessages(state, action);
        case PAGE_CHANGED:
          return processPageChange(state, action);
        case INITIALIZE_STATE:
          return action.newState;
        default:
          return state;
      }
    }
    
    function reducer(state, action) {
      const newState = computeNewState(state, action);
      if (action.type !== INITIALIZE_STATE) {
        const lfh = new LocalForageHelper(NOTIFICATIONS_CONTEXT_NAMESPACE);
        lfh.setState(newState);
      }
      return newState;
    }

which is listening for messages. I can send to that reducer from within an API call (*getMessages*() returns a Promise) in another reducer

    function processNewNotification (newNotificationVersion, notificationVersion) {
      const { version: notificationVersionNumber } = notificationVersion || {};
      const { version: newNotificationVersionNumber, hkey, rkey, is_remove: isRemove } = newNotificationVersion || {};
      if (notificationVersionNumber !== newNotificationVersionNumber) {
        getMessages().then((messages) => {
          const latest = messages.find((message) => (message.type_object_id === rkey
            && message.market_id_user_id === hkey));
          if (isRemove === _.isEmpty(latest)) {
            pushMessage(NOTIFICATIONS_HUB_CHANNEL, { event: VERSIONS_EVENT, messages });
            pushMessage(VERSIONS_HUB_CHANNEL, {event: NOTIFICATION_VERSION_UPDATE,
              notificationVersion: newNotificationVersionNumber})
          }
        });
      }
    }

that also sends a message to its own listener! The basic flow

**Promise resolves => message bus => listener(s) => call dispatch**

is very powerful. You can see production source code following this pattern [here](https://github.com/Uclusion/uclusion_web_ui/tree/master/src/contexts).

## Is there an even easier way to do this?

At the time of this writing the debate is still raging in [this bug](https://github.com/facebook/react/issues/15344). The dream is of course to use Promises within your code as if they were synchronous but that’s a tall order. In the mean time a React context / reducer and a message bus can make the data you are fetching available to your entire application without that much effort.