[JavaScript Series 7] Build Intuition for State and Rendering with a Mini Project

한국어 버전

It is time to put everything you have learned together into a small interactive project. State holds the data for the current view, render turns that data into HTML, and events reflect user actions. This article splits into the core loop (state ↔ render) and optional extras (filters, immutability); master the loop first, then add what you need.

Key terms

  1. State: the data chunk currently filling the UI, usually stored in arrays or objects.
  2. Render: the step that reads state and produces DOM nodes or HTML strings.
  3. Immutability: updating data by cloning and returning new arrays instead of mutating in place, which simplifies debugging.
  4. crypto.randomUUID: a browser API for generating unique IDs for todos.

Core ideas

  • State: collect everything the UI needs in one object so the flow stays traceable.
  • Render function: converts state into HTML and pushes it into the DOM.
  • Event loop: user action → state update → render → fresh UI.
  • Immutability: methods like map, filter, and the spread operator (...) keep updates predictable.

Code examples

Start with a micro counter that only has state and render.

const state = { count: 0 };
const output = document.querySelector(".count");

function render() {
  output.textContent = `${state.count} times`;
}

function increase() {
  state.count += 1;
  render();
}

document.querySelector("button")?.addEventListener("click", increase);
render();

➡️ Feel how “update data → call render()” is enough to keep the UI honest.

const state = {
  todos: [],
  filter: "all",
};

const elements = {
  list: document.querySelector(".todo-list"),
  form: document.querySelector(".todo-form"),
  filterButtons: document.querySelectorAll("[data-filter]"),
};

Store DOM references in one object so every function can reuse them.

function getVisibleTodos() {
  if (state.filter === "completed") return state.todos.filter((todo) => todo.completed);
  if (state.filter === "active") return state.todos.filter((todo) => !todo.completed);
  return state.todos;
}

function render() {
  const visible = getVisibleTodos();
  if (!elements.list) return;
  elements.list.innerHTML = visible
    .map(
      (todo) => `
        <li class="todo-item ${todo.completed ? "is-done" : ""}">
          <label>
            <input type="checkbox" data-id="${todo.id}" ${todo.completed ? "checked" : ""} />
            <span>${todo.title}</span>
          </label>
          <button class="todo-remove" data-id="${todo.id}">Remove</button>
        </li>
      `,
    )
    .join("");
}
function addTodo(title) {
  state.todos = [...state.todos, { id: crypto.randomUUID(), title, completed: false }];
  render();
}

function toggleTodo(id) {
  state.todos = state.todos.map((todo) =>
    todo.id === id ? { ...todo, completed: !todo.completed } : todo,
  );
  render();
}

function removeTodo(id) {
  state.todos = state.todos.filter((todo) => todo.id !== id);
  render();
}

function setFilter(filter) {
  state.filter = filter;
  render();
}

Those four functions form the core loop. Every action ends with “copy state → save new array → call render()”.

elements.form?.addEventListener("submit", (event) => {
  event.preventDefault();
  const input = elements.form.querySelector("input[name=title]");
  const value = input?.value.trim();
  if (!value) return;
  addTodo(value);
  input.value = "";
});

elements.list?.addEventListener("click", (event) => {
  const target = event.target;
  if (!(target instanceof HTMLElement)) return;
  if (target.matches("input[type=checkbox]")) toggleTodo(target.dataset.id ?? "");
  if (target.matches(".todo-remove")) removeTodo(target.dataset.id ?? "");
});

elements.filterButtons.forEach((button) => {
  button.addEventListener("click", () => {
    setFilter(button.dataset.filter ?? "all");
  });
});

render();

Each listener just reads user intent and calls the right state-update function—the same role increase played in the counter.

Why it matters

  • Separating state and render lets you rehearse “component thinking” without a framework.
  • Once you learn how events change state and render fills the DOM again, React or Svelte will feel familiar.
  • Immutability makes it obvious when state changed, which cuts debugging time.

Practice

  • Follow along: implement state, render, and the add/toggle/remove helpers to finish the todo UI.
  • Extend: persist state to localStorage or add an is-active class to filter buttons for visual feedback.
  • Debug: break immutability on purpose with state.todos.push to observe what goes wrong.
  • Done when: adding, checking, deleting, and filtering all work, and data survives a refresh.

Wrap-up

Once the loop of state → render → event → state feels natural, you can control surprisingly complex UIs without any framework. Next we will harden the input flow with form validation and feedback.

💬 댓글

이 글에 대한 의견을 남겨주세요