author: Krasimir Tsonev

Hi there, I'm . Senior front-end engineer with over 13 years of experience. I write, speak and occasionally code stuff. Follow me on Twitter, GitHub, Facebook or LinkedIn

Using JavaScript module system for state management

Using JavaScript module system for state management Photo by jeshoots

Hot topic last couple of years is state management. Especially in the front-end apps. There are lots of problems and lots of solutions. One thing thought is totally ignored in this context - the JavaScript module system. I'm very often reaching out to this approach and decided to share it here.

What we actually need when dealing with state

Let's first define the most important characteristics of a state management tool.

  • We should have access to the state from every point in our application.
  • We should be able to change the state easily.
  • We should have a mechanism for subscribing. Or in other words we should be able to execute logic when the state changes. Very often this is rendering.

And now we will try to cover those using the JavaScript module system.

Accessing the state

The module system in JavaScript has one very important feature - it caches the content of the module. Or to be more specific, it caches the stuff defined in the root scope of the file. Let's say that we have the following state.js:

// state.js
const State = {
  value: 0,
  add(n) {
    this.value += n;
  }
};
export default State;

When we import this file, we will get always the same State object. For example:

// A.js
import state from "./state";
state.add(2);

// B.js
import state from "./state";
state.add(3);

Here state is identical and if on a third place we render state.value the value will be 5.

import state from "./state";
import "./A";
import "./B";

function render() {
  // This will print out "5" on the screen.
  document.getElementById("app").innerHTML = state.value;
}
render();

This works because State is defined into the "global" state of the state.js file and as such is cached into the module system. This opens the door for various solutions based on the Singleton pattern.

Changing the state

When changing the state there is one very important thing - the amendment needs to happen via setter. We can't directly modify the value because if we do so we can't notify the rest of the application.

There are of course couple of options here. If we don't want to use a function we may rely on the MutationObserver API or use a Proxy. This doesn't really matter. The main idea is to have control on the actual state value assignment.

The subscription mechanism

This could happen easily by implementing some variants of the Publish鈥搒ubscribe pattern. For example:

let listeners = [];

const State = {
  value: 0,
  add(n) {
    this.value += n;
    listeners.forEach((c) => c());
  },
  listen(cb) {
    listeners.push(cb);
    return () => {
      listeners = listeners.filter((c) => c !== cb);
    };
  }
};

We can add a listener and every time when the value is changed we will call it. The listen method also returns a clean up function. Once fired our listeners will be removed from the list. The example app code above may be changed like so:

import state from "./state";
import "./A";
import "./B";

function render() {
  // We will get "5" and two seconds later "125".
  document.getElementById("app").innerHTML = state.value;
}

state.listen(render);

setTimeout(() => {
  state.add(120);
}, 2000);

render();

After the first render we will get 5 on the page. Then, two seconds later we will see 125. Here is an online demo https://codesandbox.io/s/wonderful-dream-vg9ge.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn. Or maybe comment below: