DEV Community

Vijay Pushkin
Vijay Pushkin

Posted on • Edited on

How State Management works? Dead simple SM in Vanilla JavaScript

Dead simple State Management in Vanilla JavaScript
It's been years now since you started using Redux, MobX or even plain React Hooks and have no idea how state management works and why it works the way it works? I'll show you the dead simple bottom level of work in state management sans any optimization or other bells and whistles.

We will be building a stupidly simple plain ol' HTML page with script tags in it.

<!DOCTYPE html>
<html>
  <head>
    <title>State Management in Vanilla JS</title>
  </head>

  <body>
    <div id="app"></div>

    <script>
      // 
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now let's write some JavaScript.

NOTE: TL;DR; is down below ⏬

const App = function _App() {  
  return `
    <h1>Hello Vanilla JS</h1>
    <div>Example of state management in Vanilla JS</div>
  `;
}
document.getElementById("app").innerHTML = App();
Enter fullscreen mode Exit fullscreen mode

I could have simply declared as

const App = function() { // ...
// or
const App = () => { // ...
Enter fullscreen mode Exit fullscreen mode

But there's reason I didn't, which I'll explain later. Now, let's create some state

App.state = {
  count: 0,
  increment: () => {
    App.state.count++;
  }
};
Enter fullscreen mode Exit fullscreen mode

A simple state created as a property on App function. πŸ˜‰

Wait! You can do that? 😲

Yes, everything in JavaScript is an object, and technically you can even do that on strings and numbers. That is why methods like "hello world".toUppercase() and (12).toFixed(2) would work. But the compiler doesn't allow you to define your own properties on a string or number.

Now that App has been made stateful, we shall integrate the state and add a click event listener at the end of file.

`
  <h1>${_App.state.count}</h1>
  <button id="button">Increase</button>
`
// ...
document.getElementById("app").innerHTML = App();
// On Click Function
document
  .getElementById("button")
  .addEventListener("click", App.state.increment);
Enter fullscreen mode Exit fullscreen mode

Note that I'm accessing App inside itself by neither this nor by App but by _App. This is called as "Named function expression"

There are two special things about Named function expression:

  1. It allows the function to reference itself internally.
  2. It is not visible outside of the function.

Even if I do something like this below, the code won't break.

const Component = App;
App = null;
document.getElementById("app").innerHTML = Component();
Enter fullscreen mode Exit fullscreen mode

Even when App has been reassigned to Component and then made to be null, the function itself remains intact and it refers itself as _App locally, hence it is not affected. Same as 'this' in every other OOP programming language (But We all know how this works in JavaScript)πŸ˜….

Now try running it (simply double click the index.html file). Notice that the on click function isn't working! πŸ™„ It's because the UI is not reflecting the latest state, let's fix that by re-rendering the elements. This can be done by running this code again when the state is updated.

document.getElementById("app").innerHTML = App();
// On Click Function
document
  .getElementById("button")
  .addEventListener("click", App.state.increment);
Enter fullscreen mode Exit fullscreen mode

Since this code is and will be repeated, we will extract it to a function

const updateTree = () => {
  document.getElementById("app").innerHTML = App();
// On Click Function
  document
    .getElementById("button")
    .addEventListener("click", App.state.increment);
}
Enter fullscreen mode Exit fullscreen mode

Now add a setState function

const setState = (callback) => {
  callback();
  updateTree(); // extracted function
}
Enter fullscreen mode Exit fullscreen mode

and update the increment function as

increment: () => {
  // Call our set state function
  setState(() => App.state.count++);
}
Enter fullscreen mode Exit fullscreen mode

Now our App works as expected. And that's it! that's the end of Dead simple State Management in Vanilla JavaScript. However just using as it is would be consider as an awful and poor framework, not because of its lack of any bell and whistles worthy feature, but because it is poorly optimised, in fact it has no optimisation, but you already know this when I said "…sans any optimization or other bells and whistles" in the beginning of this article.

Things to do,

  1. Should not render the whole application to reflect a simple change.
  2. As soon as we update to reflect the state, all the event listeners attached to DOM should not be lost and we shouldn't add new event listeners in its place.
  3. The DOM elements that were unaffected and unchanged by state should not be forced to change. Changes should be as small as possible

So we shall few optimisations to our App like how React and similar library / framework does in the next upcoming article.

TL;DR;

Here is the full HTML file we have coded so far.

<!DOCTYPE html>
<html>
  <head>
    <title>State Management in Vanilla JS</title>
  </head>

  <body>
    <div id="app"></div>

    <script>
      const App = function _App() {
        return `
          <h1>Hello Vanilla JS!</h1>
          <div>
            Example of state management in Vanilla JS
          </div>
          <br />
          <h1>${_App.state.count}</h1>
          <button id="button">Increase</button>
        `;
      };

      App.state = {
        count: 0,
        increment: () => {
          setState(() => App.state.count++);
        }
      };

      const setState = (callback) => {
        callback();
        updateTree(); // extracted function
      }

      const updateTree = () => {
        document.getElementById("app").innerHTML = App();
        document
          .getElementById("button")
          .addEventListener("click", App.state.increment);
      };

      updateTree();
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Updates:

  1. (13 Mar 2021) Added setState function, fixed few typos, added link for named function expression.

Top comments (33)

Collapse
 
artydev profile image
artydev • Edited

Here is an optimised version :-)

import {render, html} from 'uhtml';

const App = (function () {

  const State = {
    counter : 0
  }

  function incer () {
    State.counter += 1;
    App.redraw()
  }

  function view () {
    return html`
      <h1>${State.counter}</h1>
      <button class="counter" onclick=${incer}>INC</button>
      <input value = "not erased"/>`
  }

  function redraw () {
    render(app, view())
  }

  return { redraw }

})()

App.redraw()




Enter fullscreen mode Exit fullscreen mode

You can test it here : SSMOPT

Collapse
 
artydev profile image
artydev

And if you want to get a little further

import {render, html} from 'uhtml';


const Header = () => html`<h1>Header</h1>`
const Footer = () => html`<h1>Footer</h1>`
const Counter = (state, actions) => html`
      <h1>${state.counter}</h1>
      <button class="counter" onclick=${actions.inc}>INC</button>
      <input value = "not erased"/>`

const App = (function () {

  const State = {
    counter : 0
  }

  const Actions = {
    inc : () =>  {
      State.counter += 1;
      redraw();
    }
  }

  function view (s, a) {
    return html`
      ${Header()}
      ${Counter(s, a)}
      ${Footer()}
    `
  }

  function redraw () {
    render(app, view(State, Actions))
  }

  return { redraw }

})()

App.redraw()

Enter fullscreen mode Exit fullscreen mode

You can test it here IntroSamPattern

Collapse
 
reenaverma profile image
Reena Verma

Hi @artydev- this is really interesting. I've not heard of the uthml package before. What exactly is it used/needed for vs the original package?

Collapse
 
artydev profile image
artydev

Thank you for your post.

Here is a variation :

const App =(function () {

  const State = {
    counter : 0
  }

  function render () {
    app.innerHTML = view ();
    return App
  }

  function incer () {
    State.counter += 1;
    App
      .render()
      .setupEvents();
  }

  function view () {
    return `
      <div>Counter ${State.counter}</div>
      <button class="counter">INC</button>
    `
  }

  function setupEvents () {
  let button = 
    document
      .querySelector(".counter")
      .addEventListener("click", App.incer)
  }

  return { render, incer, setupEvents }

})();

App
  .render()
  .setupEvents();

Enter fullscreen mode Exit fullscreen mode

You can test it here : SSM

Regards

Collapse
 
vijaypushkin profile image
Vijay Pushkin

This one is great. Makes better use of functional programming.

Collapse
 
ryantheleach profile image
Ryan Leach

I don't want to come off sounding like an asshole, but is this satire?

It feels like someone whose never learnt JavaScript, and just copied react examples suddenly learnt how the language works... Unless I'm missing something fundamental.

Collapse
 
artydev profile image
artydev

Please, be comprehensive...

Collapse
 
rjegge profile image
rjegge

Are you sure you didn't want to come off sounding that way? I'm pretty sure you did.

Collapse
 
sgroen profile image
Viridi • Edited

Hi Vijay Pushkin,

I know this tutorial is not about the best code but I couldn't help myself from refactoring your code ;).

<!DOCTYPE html>
<html lang="en">
<head>
    <title>State Management in Vanilla JS</title>
</head>
<body>
<div id="app"></div>
<script>
  const App = {
    state: {
      count: 0,
    },
    template() {
      return `
          <h1>Hello Vanilla JS!</h1>
          <div>
            Example of state management in Vanilla JS
          </div>
          <br />
          <h1 id="counter">${this.state.count}</h1>
          <button id="button">Increase</button>
        `;
    },
    initialize(){
      document.getElementById('app').innerHTML = this.template();
      document.getElementById("button").addEventListener("click", () => App.increment());
    },
    increment() {
      this.state.count++;
      this.updateUI();
    },
    updateUI(){
      document.getElementById('counter').textContent = this.state.count;
    }
  };
  App.initialize();
</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jiminikiz profile image
Jiminikiz
<!DOCTYPE html>
<html lang="en">
  <head>
      <title>State Management in Vanilla JS</title>
  </head>
  <body>
    <main id="app">
    </main>
    <script id="template" type="text/template">
      <h1>{{title}}</h1>
      <h2 id="counter">{{count}}</h2>
      <button id="button">Increase</button>      
    </script>
    <script>
      const App = {
        state: {
          title: 'State Counter Example',
          count: 0,
        },
        template() {
          return template.innerHTML
            .replace('{{title}}', App.state.title)
            .replace('{{count}}', App.state.count);
        },
        initialize(){
          app.innerHTML = this.template();
          button.addEventListener("click", () => App.increment());
        },
        increment() {
          this.state.count++;
          this.render();
        },
        render(){
          counter.textContent = this.state.count;
        }
      };

      App.initialize();
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dapone profile image
Afisunlu Dapo • Edited

"app" in "app.innerHTML" has no reference, even though the code works perfectly. Can you please enlighten me? Pardon me me for being a novice. I was expecting something like app = document,getElementById("app")

Thread Thread
 
jiminikiz profile image
Jiminikiz

Hey Afisunlu, totally a valid question:

Both app and template exist because each DOM node that has a defined id attribute gets added to the global window object and therefore is able to be referenced without explicit initialization. This is part of the reason why id attributes should be unique for each DOM node in the tree.

Try it with another DOM node and a different id!

Collapse
 
hey_yogini profile image
Yogini Bende

This is a nice explaination and it helps you understand the state management concept behind the scene. Good work πŸ™Œ

 
artydev profile image
artydev • Edited

Hello,

here is the code of utml (877lines)

uhtml.js

You can test it here : uhtmltest

Optimized in a sense, it uses VDom Diff to update the page.

Regards

Collapse
 
snickdx profile image
Nicholas Mendez • Edited

So happy to see some framework-agnostic content. This is a great read and shows sometimes we can get by just fine with the standard web technologies.

I was really wondering why you had _App and App but your justification is brillaint.

Collapse
 
squashbugler profile image
John Grisham • Edited

Vanilla javascript? I've never heard of that library.

Collapse
 
salsferrazza profile image
Salvatore Sferrazza

Really into this, thank you for sharing! The cognitive burden of many frameworks is too high for what one gets in return.

I've found this routine to be helpful for rendering the state of arbitrary objects to the DOM. Additionally, when using standard HTML template tags, I've used another routine to populate slots (template parameters) and render the template at runtime.

Collapse
 
henshawsamuel profile image
Samuel Henshaw

Great insight

Some comments may only be visible to logged-in visitors. Sign in to view all comments.