Building a To-Do App with Vanilla JavaScript

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.