DEV Community

loading...

React Astro - Global state in React using events

Leon van der Walt
Enthusiastic full-stack dev based in South Africa. Which will it be tonight.. code or sleep? 🤔
・3 min read

Blog noob here. 💩

One morning I was bored and decided to start a new project in react. Upon setting up redux I thought of trying something new.

There has been a trend in react blogs lately explaining react's new Hooks API and different ways to implement global state.
I was wanting to implement my own custom global state solution for a while and thought this was a good opportunity.

This was partly because I wanted to see if it's possible to make something easy to use and simple to set up.

Don't get me wrong, redux is great and I don't recommend using the method in this blog for any big scale project... It's not yet thoroughly tested, purely experimental and just for fun at this stage!

My main goal on this custom implementation would be this:

  • Single source of truth
  • State that would update any components that make use of it
  • No component drilling (occurs when parent component passes data to deeply nested children, causing a domino-like update on the dom tree)

I sat on it for a while and decided on a possible solution.
I thought Using the EventEmitter class from 'events' in conjunction with a higher order component would provide an ideal solution.

I started by creating the basic structure, as well as a function to initiate the globalState object externally :

import React from 'react';
import { EventEmitter } from 'events';

const stateEmitter = new EventEmitter();
stateEmitter.setMaxListeners(0);

export let globalState = {};

export function addGlobalState(stateObj) {
  globalState = { ...globalState, ...stateObj };
}

Next up would be to create the core set function:

export const setGlobalState = param => {
  const newState = typeof param === 'function' ? param(globalState) : param;

  try {
    for (var key in newState) {
      globalState[key] = newState[key];
      stateEmitter.emit(`astroUpdate:${key}`);
    }
    stateEmitter.emit('astroUpdateState');
  } catch (error) {
    console.log('setGlobalStateError: ', error);
  }
};

The setGlobalState would take in a plain object, similar to react's setState method (the previousState implementation is also used). This would then update the globalState object with the new state object. After updating the state an event would be emitted notifying subscribed listeners that the state has changed.

Next up would be to create the capable HOC function:

export default function AstroStateProvider(WrappedComponent, keys = []) {
  return class extends React.Component {
    componentDidMount() {
      if (keys.length > 0) {
        for (const key of keys) {
          stateEmitter.on(`astroUpdate:${key}`, () => {
            this.forceUpdate();
          });
        }
      } else {
        stateEmitter.on('astroUpdateState', () => {
          this.forceUpdate();
        });
      }
    }

    render() {
      let stateToReturn = {};
      if (keys.length > 0) {
        for (const key in globalState) {
          if (keys.includes(key)) {
            stateToReturn[key] = globalState[key];
          }
        }
      } else {
        stateToReturn = globalState;
      }

      return <WrappedComponent {...this.props} astro={stateToReturn} />;
    }
  };
}

As you can see above, the hoc simply listens to any changes as set in the setGlobalState function and then runs a re-render via forceUpdate.
Also, the option exists to only listen to subscribed keys as defined by the second argument keys of AstroStateProvider.

The hoc then returns the wrapped component and the contents of the globalState object in a prop named astro.

This is also kind of an attempt at opensource contribution, so I have created a npm package based on this logic called React Astro

Installation, usage, and other minor details can be found there.

Anyone is also welcome to contribute on the Github Repo.

Cheers 🙂

Discussion (0)