Hi, I'm Abdulfetah Suudi

GitHub Logo

Software Engineer based in Ethiopia, with a passion for open source and hands-on work. Currently shipping software as a part-time software engineer and working on Rivo and Notra.

profile picture
Abdulfetah Suudi

It was January 2024. I was working through The Odin Project — specifically the "organizing your javascript code" chapter. The final exercise: build a todo list app. By this point, I'd learned prototypes, classes, compositions, npm, and webpack. It was the most challenging chapter yet.

My only tools were MDN docs and StackOverflow. No AI assistants. No auto-completion. Just me and sheer determination.

I wanted to build a simple todo list with vanilla JavaScript. Simple enough, right?

Then the DOM pain began.

Every time I added a todo, I had to create a div, a status button, a description button, attach event listeners, handle scrolling, add animations... and repeat this everywhere. It hit me — I was writing the same DOM creation code over and over again.

So I built my own component system. In vanilla JavaScript. With Webpack. Because apparently I enjoy suffering.

"I faced too much hard code repetition of DOM manipulations in vanilla javascript which I then created my own solution for, a mini react-like class constructor which generates components for me on every instantiation." — Past me, screaming inside


The Architecture

Every "component" is an ES6 class that creates its own DOM elements, attaches its own event listeners, and does its thing. Sound familiar? That's literally what React components do — I just didn't know React existed yet.


The Data Model

// src/toDo.js
export default class ToDo {
  #title;
  #id;
  #description;
  #dueDate;
  #priority;
  #note = "";
  #status = "pending";
 
  static #uuid = 1000;
 
  constructor(title, description, dueDate, priority) {
    this.#title = title;
    this.#description = description;
    this.#dueDate = dueDate;
    this.#priority = priority;
    this.#incrementUUID();
  }
 
  #incrementUUID() {
    ToDo.#uuid += 1;
    this.#id = ToDo.#uuid;
  }
 
  getId = () => this.#id;
  getTitle = () => this.#title;
  getDescription = () => this.#description;
  getDueDate = () => this.#dueDate;
  getPriority = () => this.#priority;
  getNote = () => this.#note;
  getStatus = () => this.#status;
 
  setTitle(title) {
    this.#title = title;
  }
  setDescription(description) {
    this.#description = description;
  }
  setDueDate(dueDate) {
    this.#dueDate = dueDate;
  }
  setPriority(priority) {
    this.#priority = priority;
  }
  setNote(note) {
    this.#note = note;
  }
  setStatus(status) {
    this.#status = status;
  }
}

What I was doing without knowing: Private fields (#field) acted like closure variables — only accessible inside the class. Static fields tracked a global counter. Getters read state, setters updated it. Sound like useState hooks? It should.


The Component (Renderer)

// src/NewTodo.js
export default class NewTodo {
  constructor(board, todo) {
    this.board = board;
    this.todo = todo;
    this.addTodoToDOM(); // ← Render happens here!
  }
 
  checkClicked(checkBtn) {
    const todo = checkBtn.parentElement;
    const todoNameBtn = todo.querySelector("button.description");
    const currentStatus = this.todo.getStatus();
 
    this.todo.setStatus(currentStatus === "done" ? "pending" : "done");
    todoNameBtn.dataset.status = this.todo.getStatus();
    checkBtn.dataset.status = this.todo.getStatus();
  }
 
  todoClicked(todoNode) {
    todoNode.classList.add("clicked-todo");
    import(/* webpackPrefetch: true */ "./dialog").then(
      ({ default: Dialog }) => {
        new Dialog(this.todo, todoNode);
      },
    );
  }
 
  addTodoToDOM() {
    const todoNode = document.createElement("div");
    todoNode.className = "todo";
    todoNode.dataset.id = this.todo.getId();
 
    const btn = document.createElement("button");
    btn.className = "status-checker";
    btn.dataset.status = this.todo.getStatus();
    btn.addEventListener("click", (e) => this.checkClicked(e.target));
    todoNode.appendChild(btn);
 
    const button = document.createElement("button");
    button.textContent = this.todo.getDescription();
    button.className = "description";
    button.dataset.selectedPriority = this.todo.getTitle();
    button.dataset.status = this.todo.getStatus();
    button.addEventListener("click", (e) => this.todoClicked(e.target));
    todoNode.appendChild(button);
 
    const number = this.board.children.length;
    todoNode.style.animationDuration = `${number * 10 + 500}ms`;
    todoNode.style.animationDelay = `${number * 10}ms`;
 
    this.board.appendChild(todoNode);
    this.board.scrollTo(0, this.board.scrollHeight);
  }
}

The magic: addTodoToDOM() creates elements, addEventListener() sets up interactions, dynamic imports handle code splitting. All on every instantiation. That's render() + useEffect() + React.lazy() — I just didn't know the names.


The "Router"

When you click a project or priority, Category handles view switching:

// src/category.js
export default class Category {
  main = document.querySelector("main");
 
  constructor(project, title, priority, board) {
    this.project = project;
    this.title = title;
    this.board = board;
    this.priority = priority;
    this.currentCategorySwitcher();
    this.board.innerHTML = "";
    this.board.dataset.priority = priority;
    this.displaySpecificProjectTodos();
    this.divInputController();
  }
 
  displaySpecificProjectTodos() {
    this.todos = displayProjectToDos(this.title);
    import(/* webpackPrefetch: true */ "./NewTodo").then(
      ({ default: NewTodo }) => {
        for (let i = 0; i < this.todos.length; i += 1) {
          if (
            this.priority === "all-priority" ||
            this.todos[i].getPriority() === this.priority
          ) {
            new NewTodo(this.board, this.todos[i]);
          }
        }
      },
    );
  }
}

This is a client-side router: clears the board, filters todos, renders components, shows/hides the input form. Clean.


The State Problem

No databases. No Redux. Just localStorage and a sync interval:

// src/project.js
const todos = { personal: [], work: [], grocery: [] };
const projects = [];
let local = {};
 
function sync() {
  for (let i = 0; i < projects.length; i += 1) {
    local[projects[i]] = todos[projects[i]].map((td) => [
      td.getTitle(),
      td.getDescription(),
      [
        td.getDueDate().getFullYear(),
        td.getDueDate().getMonth(),
        td.getDueDate().getDate(),
        td.getDueDate().getHours(),
        td.getDueDate().getMinutes(),
      ],
      td.getPriority(),
      td.getNote(),
      td.getStatus(),
    ]);
  }
  localStorage.setItem("local", JSON.stringify(local));
}
 
setInterval(sync, 1000);

Fun fact: I stored Dates as [year, month, date, hours, minutes] because JSON can't serialize Date objects. Brilliantly naive. The setInterval was a hack — sometimes data was stale. But it worked. I learned why frameworks exist.


Sequence Flow


What I Learned

Separating DOM from Logic: Started with everything in one file. Disaster. Classes forced separation of concerns.

Component Thinking: Each class has ONE job. ToDo → Data. NewTodo → Render. Category → Route. Dialog → Modal. DatePicker / PriorityPicker → Sub-panels.

Code Splitting Works: By using dynamic imports everywhere, initial bundle was tiny. Webpack prefetched other "pages" automatically.

State is Hard: That 1-second sync? A hack. I learned why proper state management matters.


The "Aha!" Moment

I didn't know React's function components, useState, or useEffect. But I independently discovered the same patterns through pure frustration:

My Vanilla JSReact Equivalent
class ToDo { #field }useState()
addEventListener() in constructoruseEffect(() => { ... }, [])
setInterval(sync, 1000)useSyncExternalStore
new NewTodo()<NewTodo />
import()React.lazy()

The parallel is incredible — I was building hooks before hooks existed in React (2019)!

That's programming: the problems are universal, and if you think hard enough, you'll arrive at similar solutions.


Would I Do It Differently?

Maybe not.

This taught me how frameworks work under the hood, why state management matters, why code splitting is important, how bundlers optimize. If I'd used React from the start, I'd have typed npx create-react-app, followed tutorials, and never understood WHY React does what it does.

My vanilla JS "React-like" library was messy, buggy, and inefficient.

And it was beautiful.

Every expert was once a beginner who refused to give up — and built things the hard way first.



You will never be perfect, but you can always improve!

No sponsors yet. Be the first!

Become a Sponsor