DEV Community

loading...

How State Management works? Dead simple SM in Vanilla JavaScript

vijaypushkin profile image Vijay Pushkin ใƒปUpdated on ใƒป4 min read

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.

Discussion (28)

pic
Editor guide
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
mkimont profile image
Matt Kimek

Could you explain me how this is optimized as I see that you injecting uhtml which is 2.5k minimize which is thousands of lines?

Thread Thread
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

Thread Thread
mkimont profile image
Matt Kimek

Basically we adding more code, adding render everything to change one little variable.
This doesn't seems like optimisation for me.
Better give a go for lit-html.polymer-project.org/ and check youtube.com/watch?v=uCHZJy2n8Qs from google podcast which compare vdom and lit

Thread Thread
artydev profile image
artydev

Thank you
I won t give you any links.
There is enough material on Dev To, that explain my choice.
Regards

Thread Thread
mkimont profile image
Matt Kimek

Is like saying there is Internet to explain my thoughts. If chrome developers not convinced you then well... "Thank you for constructive discussion"

Thread Thread
artydev profile image
artydev

Hello Matt,
Please ask real questions, read authors posts, share your tips, examples
That will be constructive for all of us
Regards

Thread Thread
sgroen profile image
Viridi • Edited

First of all great post Vijay!

artydev is right about the fact that the code could be optimized but Matt is also right. Adding a library does not optimize this piece of code.
Having a look at the code there is only one thing that changes and that is text in the last h1 tag. So if you want to optimize this code why not do it like this?

<!DOCTYPE html>
<html lang="en">
<head>
    <title>State Management in Vanilla JS</title>
</head>
<body>
<h1>Hello Vanilla JS!</h1>
<div>
    Example of state management in Vanilla JS
</div>
<br />
<h1 id="app"></h1>
<button id="button">Increase</button>
<script>
    const App = function _App() {
        return _App.state.count;
    };

    App.state = {
        count: 0,
        increment: () => {
            App.state.count++;
            App.state.updateUI();
        },
        updateUI: () => {
            document.getElementById("app").textContent = App();
        }
    };
    App.state.updateUI();
    document.getElementById("button").addEventListener("click", App.state.increment);
</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

There is no need to add the event listener on every update and there is no need to rerender the rest of the html on every update. Just update the text in the h1 tag and minimize the rerendering needs. If you want to build the html from javascript you can render the html just once on initialization.

Thread Thread
artydev profile image
artydev • Edited

Thank you for your tips
You are totally right if you have
few elements to update.

Regards

Thread Thread
sgroen profile image
Viridi • Edited

No problem artydev. You can of course always fall back on a library when you have a lot of re-rendering going on. I believe that to make the right decision you need to understand what is going on under the hood. There are a lot of tutorials on diffing and VDom to be found on the internet for those interested.

Thread Thread
vijaypushkin profile image
Vijay Pushkin Author

Hi there Viridi. Thanks for your input.

But optimising and moving all the elements to HTML wasn't the point of this article

Thread Thread
sgroen profile image
Viridi

Hi Vijay Pushkin,

You are absolutely right. artydev and Matt Kimek started a discussion about optimization. I just hooked in to that. Your post was about handling state in vanilla javascript and you did a good job on that.

Thread Thread
artydev profile image
artydev • Edited

And only for the pleasure , here is a case when using a library worth it :-)

<div class="my-chrono"></div>
<div class="my-chrono"></div>
<div class="my-chrono"></div>
<div class="my-chrono"></div>
<div class="my-chrono"></div>
<div class="my-chrono"></div>
Enter fullscreen mode Exit fullscreen mode
import {define} from 'wicked-elements';

let Counter = function () {
  let count = 0;  
  let disabled = false;
  let timer;
  let start = () => {timer = setInterval(() => { count += 1; disabled = true; m.redraw()}) };
  let stop = () => {clearInterval(timer); disabled = false; m.redraw()};
  let reset = () => {stop(); count = 0};
  let disable = (status) => status;
  let view = () => [
    m(".wrap", 
      m("h1",  count ), 
        m("",
        m("button", {onclick : start, disabled : disabled}, "start"),
        m("button", {onclick : stop}, "stop"),
        m("button", {onclick : reset}, "reset")
      ) 
    )
  ]
  return  { view }
}

function renderComp(target, component) {
   m.mount(target, component);
}

define(".my-chrono", {
   init() {
     renderComp(this.element, Counter);
   } 
})

Enter fullscreen mode Exit fullscreen mode

You can test it here :

MulipleCounters

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 Author

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

Collapse
ms_yogii profile image
Yogini Bende

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

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
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
henshawsamuel profile image
Samuel Henshaw

Great insight

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
shtep profile image
jim shtepa

Is your lastname really Pushkin? Like Alexander Sergeyevich Pushkin?

Collapse
vijaypushkin profile image
Vijay Pushkin Author

The place where I'm from, we don't have family names. So, yes both are my names!

Collapse
shtep profile image
jim shtepa

Understood, cool. Pushkin is perhaps the greatest person in Russian literature. I am from Kazakhstan and we happen to speak and learn russian language at school. Anyways you have a great name!

Collapse
saksham profile image
Saksham96

Nice Insight on state management, waiting for the upcoming article.

Collapse
paramsiddharth profile image
Param Siddharth • Edited

Wow! ๐Ÿ˜ฎ๐Ÿ˜๐Ÿฅฐ What an amazing tutorial! Now I can start writing my own front-end web framework. ๐Ÿ˜๐Ÿ˜๐Ÿ˜‚๐Ÿ˜‚ Just kidding, I won't! But I've always wanted to know how to. Thank you for the article! ๐Ÿ’•๐Ÿ’•

Collapse
squashbugler profile image
John Grisham • Edited

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