DEV Community

Cover image for How to implement state management inside a simple JavaScript app
AILI Fida Aliotti Christino
AILI Fida Aliotti Christino

Posted on

How to implement state management inside a simple JavaScript app

Motivation

Not every application needs React or Angular or another fancy, trendy framework.

What i see in every junior developer is that they are tackling every problem using frameworks even though there's no need to use them at some point.

So in this article, i will show you how to handle your application using Vite and JavaScript (my article, my rules 😋)

The main structure

Okay, let's start from the beginning, the core of our app by running this simple command:

yarn create vite
Enter fullscreen mode Exit fullscreen mode

Of course, you can use any package manager you wish to.

Give a project name when asked, in my case i choose "greatapp" (not an original app name but deal with it, ok? 😆), then choose Vanilla and JavaScript in the last question.

So, now you should have something like this:

folder structure

Let's run the app by typing yarn dev

Here is what we got:

first run

Go on and try to click on the button. It works! The amount is incrementing!

Hum... but what if i want to decrement it? 😅
Still following? Great! Let's jump into the next section

Components

Apps are often divided into small pieces that we call Components. Separating our code into Components will help use to maintain and grow our app easily and even debug.

Let's look at the counter.js file. If you are coming from a React background, you can see that it is similar to a component which have a state changing when an action is done.

Well, what i want to do is to isolate buttons into components and assign actions to them.

Let's create it. Start by creating a file named Button.js (you can choose another name but it's more relevant to me to call it like that 😎)

Now let's add some code inside it.

export const Button = (element, text) => {
  const button = document.createElement("button");
  button.innerHTML = text;
  document.querySelector(element).appendChild(button);
};
Enter fullscreen mode Exit fullscreen mode

Ok, let's break it down for a minute. We are creating a component called Button with elements and text as parameters where element is the HTML element we will add our component to, and text will be the text displayed on the button.

Let's use it inside our main function by changing it to make it use our newly created component:

import "./style.css";
import javascriptLogo from "./public/javascript.svg";
import { setupCounter } from "./counter.js";
import { Button } from "./buttons";

document.querySelector("#app").innerHTML = `
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
      <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
    </a>
    <h1>Hello Vite!</h1>
    <div class="card">
      <button id="counter" type="button"></button>
      <p id="counter"></p>
    </div>
    <div id="buttons"></div> 
  </div>
`;

setupCounter(document.querySelector("#counter"));

Button("#buttons", "-");
Button("#buttons", "+");
Enter fullscreen mode Exit fullscreen mode

Oh my! What's going on here 😱! We've added our Button component by importing it and we've add two instance of it at the bottom. As you can see, Button components will be rendered inside a div tag created above:

<div id="buttons"></div>
Enter fullscreen mode Exit fullscreen mode

Also, i've changed the element counter button into a simple paragraph element as we will change its state by using our Button component

With the style added by default by Vite, our buttons will look so fancy! 😍

buttons added

Actions

Okay, we've added our buttons but they are useless for now. Let's change that and add some events to our Button component:

export const Buttons = (element, text, action) => {
  const button = document.createElement("button");
  button.innerHTML = text;
  button.addEventListener("click", action);
  document.querySelector(element).appendChild(button);
};
Enter fullscreen mode Exit fullscreen mode

Now we've added an action which is a function executed when the user will click on our button. Let's try again to use it so we make sure it works by changing our main.js

import "./style.css";
import javascriptLogo from "./javascript.svg";
import { setupCounter } from "./counter.js";
import { Buttons } from "./buttons";

document.querySelector("#app").innerHTML = `
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
      <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
    </a>
    <h1>Hello Vite!</h1>
    <div class="card">
      <button id="counter" type="button"></button>
    </div>
    <div id="buttons"></div>
  </div>
`;

const action = () => {
  console.log("Hello from button");
};

setupCounter(document.querySelector("#counter"));
Buttons("#buttons", "-", action);
Enter fullscreen mode Exit fullscreen mode

When we click on our buttons, we can see that our function is running correctly as it will display Hello from button inside the console.

Great! Let's try to add a state management to handle this variable.

State management

1- What is a state management?

State management refers to the process of managing data or state in an application, particularly in single-page applications or in applications with complex user interfaces.

The goal of state management is to ensure that the data displayed to the user and the user interactions with the application are consistent and predictable, even as the data changes over time.

I hope this is clear enough 😊

2- Attempt 1 - Global variables

Ok, first, we can think of a way to implement this by creating a file holding the global variables and call them inside other file. Seems great? Let's try!

Create a file at the root of our project, i will name it state.js (again you can name it whatever you want) and let's put this code inside it:

// state.js
export const $counter = 0; // "$" sign is just a way for me to differenciate global variables
Enter fullscreen mode Exit fullscreen mode

Basically, we are just exporting a simple variable with an initial state of 0.
Now, let's use it inside our app:

// counter.js
import { $counter } from "./state";

export function setupCounter(element) {
  const setCounter = () => {
    element.innerHTML = `count is ${$counter}`;
  };

  setCounter();
}
Enter fullscreen mode Exit fullscreen mode
// main.js

import "./style.css";
import javascriptLogo from "./javascript.svg";
import { setupCounter } from "./counter.js";
import { Buttons } from "./buttons";
import { $counter } from "./state";

document.querySelector("#app").innerHTML = `
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
      <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
    </a>
    <h1>Hello Vite!</h1>
    <div class="card">
      <p id="counter"></p>
    </div>
    <div id="buttons"></div> 
  </div>
`;

const increment = () => {
  $counter = $counter + 1;
};

const decrement = () => {
  $counter = $counter - 1;
};

setupCounter(document.querySelector("#counter"));
Button("#buttons", "-", decrement);
Button("#buttons", "+", increment);
Enter fullscreen mode Exit fullscreen mode

🥵 ouf! Now, let's do some test!
❌ Uh-oh 😱! What's this? I'm getting an explicit error saying Assignment to constant variable.

Hum...🤔 Oh silly me! Of course, we can't change a constant variable! Let's change this before someone notices 😁

export let $counter = 0;
Enter fullscreen mode Exit fullscreen mode

Now that's better 😊! Test again... nooo! This error again? May be restarting our server will change something but... it's useless. JavaScript is driving me crazy 😵‍💫

Let's do some Googling stuff ⌨️ ... owww 😲! It's been said that every imported variable is immutable which means that we cannot change it.

So, what's going on here? It seems that as soon as we are importing a variable from somewhere, that same variable is immutable. 🤔 Didn't know that. But as a great developer you can find some solution to that problem.

3- Attempt 2 - Proxy

Okay, first attempt didn't work. Let's try to use proxies to help us deal with this assignation error.

Hum? Proxy? Well, a Proxy object makes it possible to create an intermediary for another object and which can intercept and redefine certain fundamental operations for it.

Enough chit-chat, let's get back to code! Let's change our state.js file to this:

import { setupCounter } from "./counter";

const handler = {
  set: function (obj, prop, value) {
    obj[prop] = value;
    setupCounter(document.getElementById("counter"));
    return true;
  },
};

export const state = new Proxy(
  {
    counter: 0,
  },
  handler
);

export const actions = {
  increment: () => {
    state.counter++;
  },
  decrement: () => {
    state.counter--;
  },
};
Enter fullscreen mode Exit fullscreen mode

And now, let's adapt our main.js file to match these changes:

import "./style.css";
import javascriptLogo from "./javascript.svg";
import { setupCounter } from "./counter.js";
import { Button } from "./button";
import { actions, state } from "./state";

const App = () => {
  return `<div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
      <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
    </a>
    <h1>Hello Vite!</h1>
    <div class="card">
      <p id="counter"></p>
    </div>
    <div id="buttons"></div> 
  </div>`;
};

document.querySelector("#app").innerHTML = App();

setupCounter(document.getElementById("counter"));
Button("#buttons", "-", actions.decrement);
Button("#buttons", "+", actions.increment);
Enter fullscreen mode Exit fullscreen mode

Slight change to App declaration to match our beloved React app structure 😅
Let's see if it works... and ... it WORKS! 🎉

That's it folks! We've implemented state management from scratch using JavaScript.

There is a package developed around this proxy concept called Valtio which is available for JavaScript, React, and so on... feel free to check and star it on Github.

Conclusion

This was not a very shiny demonstration of the proxy potential as what we did is just incrementing and decrementing a counter, but if you want a more advanced use of it, here is a link to a todo project using the same concept.

Cheers! 🍻🍻🍻

Top comments (0)