One-way state management in vanilla JavaScript
Tags: javascript
Published: 2021-08-17 17:13:22

---
title: One-way state management in vanilla JavaScript
published: true
description: 
tags: javascript
cover_image: https://images.unsplash.com/photo-1608591294306-ed822a53831b?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1400&q=80
canonical_url: https://www.antoniovdlc.me/one-way-state-management-in-vanilla-javascript/
---

Ever wonder what were the basic building blocks of one-way state management libraries such as `redux` or `vuex`? Well, you are in the right place as we will be looking at re-implementing one-way state management in vanilla JavaScript. 

---

For the purpose of this article, we will be building a basic counter, with a button to increment the counter, a button to decrement the counter, and a button to reset the counter.

The basic markup we will be working with is the following:

`index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Counter</title>
</head>
<body>
  <p id="counter"></p>
  <button id="increment">+</button>
  <button id="reset">Reset</button>
  <button id="decrement">-</button>

  <script src="main.js"></script>
</body>
</html>
```

The goal is to look at different implementations of managing the state of the counter.

---

Let's first start with a naive implementation:

`main.js`
```js
window.addEventListener("DOMContentLoaded", ignite);

function ignite() {
  const $counter = document.querySelector("#counter");
  const $increment = document.querySelector("#increment");
  const $decrement = document.querySelector("#decrement");
  const $reset = document.querySelector("#reset");

  const state = {
    counter: 0,
  };

  $increment.addEventListener("click", () => {
    state.counter = state.counter + 1
    $counter.innerText = state.counter;
  });
  $decrement.addEventListener("click", () => {
    state.counter = state.counter - 1
    $counter.innerText = state.counter;
  });
  $reset.addEventListener("click", () => {
    state.counter = 0
    $counter.innerText = state.counter;
  });
}
```

We are attaching event listeners on each button, and mutating the `counter` field of a `state` object that is in scope of all the event handlers. This works fine, but we are already seeing a few places where this code might not scale so well. 

---

The most obvious one is that we need to set the counter's inner text in each handler:

```js
$counter.innerText = state.counter;
```

It would be great if we could abstract that away in a function, such as:

```js
function updateUI() {
  $counter.innerText = state.counter;
}
```

Now our overall code looks like follows:

```js
window.addEventListener("DOMContentLoaded", ignite);

function ignite() {
  const $counter = document.querySelector("#counter");
  const $increment = document.querySelector("#increment");
  const $decrement = document.querySelector("#decrement");
  const $reset = document.querySelector("#reset");

  function updateUI() {
    $counter.innerText = state.counter;
  }

  const state = {
    counter: 0,
  };

  $increment.addEventListener("click", () => {
    state.counter = state.counter + 1;
    updateUI();
  });
  $decrement.addEventListener("click", () => {
    state.counter = state.counter - 1;
    updateUI();
  });
  $reset.addEventListener("click", () => {
    state.counter = 0;
    updateUI();
  });
}
```

This is an improvement as we only need to update the `updateUI()` function if we scale the counter and need to make more changes to the UI when the counter's value updates, but this is not yet as DRY as I could be ...

---

Enter, `Proxies`!

To automatically make a call to `updateUI()` whenever any field in the `state` gets updated, we will wrap the `state` object in a `Proxy`:

```js
  const state = new Proxy(
    {
      counter: 0,
    },
    {
      set(obj, prop, value) {
        obj[prop] = value;
        updateUI();
      },
    }
  );
```
> Note that we could have also used an implementation of the Observer pattern, but this is less code!

Now, every time that a field in the `state` gets update, we will call `updateUI()`. This leaves us with the following code:

```js
window.addEventListener("DOMContentLoaded", ignite);

function ignite() {
  const $counter = document.querySelector("#counter");
  const $increment = document.querySelector("#increment");
  const $decrement = document.querySelector("#decrement");
  const $reset = document.querySelector("#reset");

  function updateUI() {
    $counter.innerText = state.counter;
  }

  const state = new Proxy(
    {
      counter: 0,
    },
    {
      set(obj, prop, value) {
        obj[prop] = value;
        updateUI();
      },
    }
  );

  $increment.addEventListener("click", () => {
    state.counter = state.counter + 1;
  });
  $decrement.addEventListener("click", () => {
    state.counter = state.counter - 1;
  });
  $reset.addEventListener("click", () => {
    state.counter = 0;
  });
}
```

Alright, that looks pretty neat ... but having direct mutations to the `state` still doesn't look that scalable and easy to reason about once we start adding more complex asynchronous interactions.

---

This is where one-way state management libraries really shine. Sure, it's a lot of boilerplate, and it might not make sense for simple (even asynchronous) applications, but it also brings predictability while managing state.

Ok, so let's go step by step. In most one-way state management libraries, there is a central `store` which has a private `state` and exposes a `dispatch()` and a `getState()` function. To mutate the `state`, we `dispatch()` actions, which call the main `reducer()` to produce the next `state` depending on the actual value and the action being dispatched. The `state` cannot be mutated outside of the `store`.

To achieve such a design, we have to create a closure around a `state` object, by first building a function that will create a store:

```js
  function createStore(initialState, reducer) {
    const state = new Proxy(
      { value: initialState },
      {
        set(obj, prop, value) {
          obj[prop] = value;
          updateUI();
        },
      }
    );

    function getState() {
      // Note: this only works if `initialState` is an Object
      return { ...state.value };
    }

    function dispatch(action) {
      const prevState = getState();
      state.value = reducer(prevState, action);
    }

    return {
      getState,
      dispatch,
    };
  }
```

Here, we have moved our previous proxied version of `state` inside the `createStore()` function, which accepts 2 arguments: the initial value of `state` and the main `reducer` used to calculate the next state depending on the dispatched action. 

It returns an object with a `getState()` function, which returns an "unproxied" value for `state`. Among other things, this ensures that `state` is never mutated outside of the `reducer()` as the value returned is not the actual `state` held by the `store`.

The `dispatch()` function, takes in an `action` and calls the main `reducer()` with the previous value of `state` and said `action`, then assigns the newly returned `state`.

In our case, we can define the `initalState` and the `reducer()` as follows:

```js
  const initialState = { counter: 0 };

  function reducer(state, action) {
    switch (action) {
      case "INCREMENT":
        state.counter = state.counter + 1;
        break;
      case "DECREMENT":
        state.counter = state.counter - 1;
        break;
      case "RESET":
      default:
        state.counter = 0;
        break;
    }
	
	return state;
  }
```

Note that in our case, reducers are pure functions, so they need to return the new value of the `state`.

Finally, we initialize the `store`, and make the necessary changes to our event handlers and `updateUI()` function:

```js
  const store = createStore(initialState, reducer);

  function updateUI() {
    $counter.innerText = store.getState().counter;
  }

  $increment.addEventListener("click", () => {
    store.dispatch("INCREMENT");
  });
  $decrement.addEventListener("click", () => {
    store.dispatch("DECREMENT");
  });
  $reset.addEventListener("click", () => {
    store.dispatch("RESET");
  });
```

---

All together, our homemade one-way state management in vanilla JavaScript to handle a counter looks like this:

`main.js`
```js
window.addEventListener("DOMContentLoaded", ignite);

function ignite() {
  const $counter = document.querySelector("#counter");
  const $increment = document.querySelector("#increment");
  const $decrement = document.querySelector("#decrement");
  const $reset = document.querySelector("#reset");

  function createStore(initialState, reducer) {
    const state = new Proxy(
      { value: initialState },
      {
        set(obj, prop, value) {
          obj[prop] = value;
          updateUI();
        },
      }
    );

    function getState() {
      // This only works if `initialState` is an Object
      return { ...state.value };
    }

    function dispatch(action) {
      const prevState = getState();
      state.value = reducer(prevState, action);
    }

    return {
      getState,
      dispatch,
    };
  }

  const initialState = { counter: 0 };

  function reducer(state, action) {
    switch (action) {
      case "INCREMENT":
        state.counter = state.counter + 1;
        break;
      case "DECREMENT":
        state.counter = state.counter - 1;
        break;
      case "RESET":
      default:
        state.counter = 0;
        break;
    }

    return state;
  }

  const store = createStore(initialState, reducer);

  function updateUI() {
    $counter.innerText = store.getState().counter;
  }

  $increment.addEventListener("click", () => {
    store.dispatch("INCREMENT");
  });
  $decrement.addEventListener("click", () => {
    store.dispatch("DECREMENT");
  });
  $reset.addEventListener("click", () => {
    store.dispatch("RESET");
  });
}
```

---

Of course, libraries like `redux` or `vuex` take care of a lot of edge cases that we have overlooked, and add a lot more to the mix than just the concepts we have touched upon in the article, but hopefully that gives you a good idea of the logic behind some popular one-way state management libraries.