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
- State: the data chunk currently filling the UI, usually stored in arrays or objects.
- Render: the step that reads state and produces DOM nodes or HTML strings.
- Immutability: updating data by cloning and returning new arrays instead of mutating in place, which simplifies debugging.
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 theadd/toggle/removehelpers to finish the todo UI. - Extend: persist state to
localStorageor add anis-activeclass to filter buttons for visual feedback. - Debug: break immutability on purpose with
state.todos.pushto 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.
💬 댓글
이 글에 대한 의견을 남겨주세요