The same task, two approaches.
Interactive demo
Below is the same todo list built twice: once in vanilla JavaScript, once in React. The live list is at the top; under it, the two implementations sit side by side (tabbed on narrow screens). Before you read them: which version do you think is shorter, and by how much?
What To Do?
No todos yet. Type something and press Enter or click Add.
<script>
const input = document.getElementById("todo-input");
const addBtn = document.getElementById("add-btn");
const list = document.getElementById("todo-list");
const counter = document.getElementById("counter");
let nextId = 0;
function updateCounter() {
const total = list.children.length;
const done = list.querySelectorAll(".completed").length;
counter.textContent = done + "/" + total + " completed";
}
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
const id = nextId++;
const li = document.createElement("li");
li.className = "todo-item";
li.dataset.id = id;
// Toggle button
const toggleBtn = document.createElement("button");
toggleBtn.textContent = "○";
toggleBtn.addEventListener("click", () => {
li.classList.toggle("completed");
const span = li.querySelector("span");
if (li.classList.contains("completed")) {
toggleBtn.textContent = "✓";
span.style.textDecoration = "line-through";
span.style.opacity = "0.5";
} else {
toggleBtn.textContent = "○";
span.style.textDecoration = "";
span.style.opacity = "";
}
updateCounter();
});
// Text
const span = document.createElement("span");
span.textContent = text;
// Delete button
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "✕";
deleteBtn.addEventListener("click", () => {
list.removeChild(li);
updateCounter();
});
li.appendChild(toggleBtn);
li.appendChild(span);
li.appendChild(deleteBtn);
list.appendChild(li);
input.value = "";
updateCounter();
});
</script>
<!-- HTML markup -->
<div id="todo-app">
<div>
<input id="todo-input" type="text" placeholder="Add a todo..." />
<button id="add-btn">Add</button>
</div>
<p id="counter"></p>
<ul id="todo-list"></ul>
</div>function TodoList() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState("");
function addTodo() {
if (!text.trim()) return;
setTodos([...todos, { id: Date.now(), text, done: false }]);
setText("");
}
function toggle(id) {
setTodos(todos.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
));
}
function remove(id) {
setTodos(todos.filter((t) => t.id !== id));
}
const done = todos.filter((t) => t.done).length;
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={addTodo}>Add</button>
<p>{done}/{todos.length} completed</p>
<ul>
{todos.map((t) => (
<li key={t.id}>
<button onClick={() => toggle(t.id)}>
{t.done ? "✓" : "○"}
</button>
<span style={{ opacity: t.done ? 0.5 : 1 }}>
{t.text}
</span>
<button onClick={() => remove(t.id)}>✕</button>
</li>
))}
</ul>
</div>
);
}<script>
const input = document.getElementById("todo-input");
const addBtn = document.getElementById("add-btn");
const list = document.getElementById("todo-list");
const counter = document.getElementById("counter");
let nextId = 0;
function updateCounter() {
const total = list.children.length;
const done = list.querySelectorAll(".completed").length;
counter.textContent = done + "/" + total + " completed";
}
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
const id = nextId++;
const li = document.createElement("li");
li.className = "todo-item";
li.dataset.id = id;
// Toggle button
const toggleBtn = document.createElement("button");
toggleBtn.textContent = "○";
toggleBtn.addEventListener("click", () => {
li.classList.toggle("completed");
const span = li.querySelector("span");
if (li.classList.contains("completed")) {
toggleBtn.textContent = "✓";
span.style.textDecoration = "line-through";
span.style.opacity = "0.5";
} else {
toggleBtn.textContent = "○";
span.style.textDecoration = "";
span.style.opacity = "";
}
updateCounter();
});
// Text
const span = document.createElement("span");
span.textContent = text;
// Delete button
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "✕";
deleteBtn.addEventListener("click", () => {
list.removeChild(li);
updateCounter();
});
li.appendChild(toggleBtn);
li.appendChild(span);
li.appendChild(deleteBtn);
list.appendChild(li);
input.value = "";
updateCounter();
});
</script>
<!-- HTML markup -->
<div id="todo-app">
<div>
<input id="todo-input" type="text" placeholder="Add a todo..." />
<button id="add-btn">Add</button>
</div>
<p id="counter"></p>
<ul id="todo-list"></ul>
</div>Both do the same thing. The framework version is shorter, declarative, and automatically keeps the UI in sync with the data.
What to notice
The live demo is the same in both worlds: a todo list you can add to, complete, and delete. The code underneath is where they part ways.
Same behavior, less than half the hand-written wiring, and that gap only widens as the UI grows. The framework lets you spend your attention on what to show instead of how to update the page.
Next, explore the framework landscape