A to-do app is the “hello world” of frontend development for good reason — it touches DOM manipulation, event handling, and state management in one small, understandable project.
const state = { todos: JSON.parse(localStorage.getItem('todos') || '[]') };
function render() {
const list = document.querySelector('#todo-list');
list.innerHTML = state.todos.map((todo, i) => <code>
<li class="${todo.done ? 'done' : ''}">
<input type="checkbox" data-index="${i}" ${todo.done ? 'checked' : ''}>
<span>${todo.text}</span>
<button data-remove="${i}">×</button>
</li>
</code>).join('');
localStorage.setItem('todos', JSON.stringify(state.todos));
}
document.querySelector('#add-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.querySelector('#todo-input');
state.todos.push({ text: input.value, done: false });
input.value = '';
render();
});
document.querySelector('#todo-list').addEventListener('click', (e) => {
if (e.target.matches('[data-remove]')) {
state.todos.splice(e.target.dataset.remove, 1);
render();
}
if (e.target.matches('[type=checkbox]')) {
state.todos[e.target.dataset.index].done = e.target.checked;
render();
}
});
render();
This “single source of truth plus a render function” pattern — mutate state, then re-render the whole list from scratch — is a genuinely useful simplification, and it’s the exact same mental model React and Vue formalize with virtual DOM diffing under the hood.