Vanilla Hooks

A tiny library (less than 50 lines of code) that brings React-like hooks to vanilla JavaScript.

View source code on GitHub

How It Works

The first thing I did was create a global variable to prevent polluting the window object.

vanilla-hooks.js
window.VanillaHooks = {};
      

Next, I added some properties and methods to manage states and effects.

vanilla-hooks.js
window.VanillaHooks = {
  states: [],
  State: class {},
  useState: () => {},
  useEffect: () => {},
};
      

The constructor on the State class initializes the value and pushes an event listener to the states array.

vanilla-hooks.js
constructor(intialValue) {
  this.value = intialValue;
  const { length: index } = window.VanillaHooks.states;
  this.id = `vanilla-state-${index}`;
  window.VanillaHooks.states.push(new Event(this.id));
  this.event = window.VanillaHooks.states[index];
}
      

Within useState, I have a setState function that dispatches the event when the state changes.

vanilla-hooks.js
const setState = (parameter) => {
  const isFunction = typeof parameter === "function";
  const value = isFunction ? parameter(state.value) : parameter;
  state.set(value);
  dispatchEvent(state.event);
};
      

Finally, the useEffect method adds an event listener using the callback for all the dependencies.

vanilla-hooks.js
dependencies.forEach((state) => addEventListener(state.id, callback));
      

Practical Examples

Here are some practical examples of using Vanilla Hooks:

Decrement & Increment

This example allows users to increment and decrement a counter.

index.html
<button onclick="handleDecrement()">Decrement</button>
<span id="count"></span>
<button onclick="handleIncrement()">Increment</button>
      
app.js
const { useEffect, useState } = VanillaHooks;

const countElement = document.getElementById("count");

const [count, setCount] = useState(1);

const handleIncrement = () => setCount((previousState) => ++previousState);
const handleDecrement = () => setCount((previousState) => --previousState);

useEffect(() => {
  countElement.innerText = count;
}, [count]);
    

Input Binding

This example binds an input field to a state variable and displays the value in real-time.

Name:

index.html
<p>
  <span><strong>Name: </strong></span>
  <span id="user-text"></span>
</p>
<input type="text" id="user-input" onkeyup="handleUserChange(event)" />
      
app.js
const { useEffect, useState } = VanillaHooks;

const userTextElement = document.getElementById("user-text");
const userInputElement = document.getElementById("user-input");

const [user, setUser] = useState("Jane Doe");

const handleUserChange = (event) => setUser(event.target.value);

useEffect(() => {
  userTextElement.innerText = user;
  userInputElement.value = user;
}, [user]);
    

Todo List

This example manages a list of items and allows a user to add/remove tasks.

index.html
<ul id="todo-list"></ul>
<input type="text" id="todo-input" />
<button id="add-todo-button">Add Todo</button>
      
app.js
const { useEffect, useState } = VanillaHooks;

const todoListElement = document.getElementById("todo-list");
const todoInputElement = document.getElementById("todo-input");
const addTodoButtonElement = document.getElementById("add-todo-button");

const [items, setItems] = useState(["Write more JavaScript"]);

addTodoButtonElement.addEventListener("click", () =>
  setItems((previousState) => [...previousState, todoInputElement.value])
);

useEffect(() => {
  todoListElement.innerHTML = "";
  todoInputElement.value = "";
  items.map((item, index) => {
    const todoItem = document.createElement("li");
    const removeItemButtonElement = document.createElement("button");
    removeItemButtonElement.innerText = "Remove";
    removeItemButtonElement.addEventListener("click", () => {
      setItems((previousState) => {
        previousState.splice(index, 1);
        return previousState;
      });
    });
    todoItem.innerText = item;
    todoItem.appendChild(removeItemButtonElement);
    todoListElement.appendChild(todoItem);
  });
}, [items]);