Vue Tutorial · Lessons

Foundation

Basic

Medium

Advanced

Build a To-Do App

A complete small app: add, edit, complete, filter and persist items.

The to-do list is the "hello world" of front-end frameworks because it uses nearly everything you need day to day: rendering a list, a form, events, derived state, editing in place and saving data. This lesson builds one in six small steps. Each step is a Try it yourself editor you can change and re-run, and the finished app is at the bottom.

Each editor is self-contained. The Script box is the body of setup(), and Vue's ref, computed, watch and so on are in scope.

Step 1: state and a list

Start with the data. A to-do is an object with an id, title and done flag, and the list is a ref holding an array. v-for renders it, with :key set to the id so Vue can track each row.

Step 2: adding items

A form with v-model on the input and @submit.prevent on the form. .prevent stops the browser reloading the page. Trim the input and ignore empty titles.

Step 3: completing and removing

Bind a checkbox straight to todo.done with v-model: Vue's reactivity tracks changes to objects inside the array. Removing is a filter that keeps every item except the one clicked.

Step 4: filters and counts with computed

"All / Active / Completed" filters and the "2 items left" counter are derived state: they're calculated from todos, so they belong in computed(), not in extra refs you'd have to keep in sync.

Step 5: editing in place

Double-click a title to edit it. Track which item is being edited (editingId), show an input for that row only, and save on Enter or blur. Escape cancels.

Step 6: saving to localStorage

A deep watch on the list writes it to localStorage whenever anything changes, and the initial value is read back when the component starts. Wrap storage access in try/catch: it can throw in private windows or when storage is full.

Add a few items, then press Run (which re-creates the app from scratch): your list comes back.

The finished app

Everything above, put together as two components: a TodoApp that owns the state, and a TodoItem that receives a todo as a prop and emits toggle, remove and rename events (see Props & Emits). Your items are saved in this browser.

The component split looks like this in a real project:

<!-- TodoItem.vue -->
<script setup>
import { ref, nextTick, useTemplateRef } from 'vue'   // useTemplateRef is Vue 3.5+

const props = defineProps({ todo: { type: Object, required: true } })
const emit = defineEmits(['toggle', 'remove', 'rename'])

const editing = ref(false)
const draft = ref('')
const input = useTemplateRef('input')

async function startEdit() {
  draft.value = props.todo.title
  editing.value = true
  await nextTick()          // wait for the <input> to render
  input.value.focus()
}

function finish() {
  if (!editing.value) return
  editing.value = false
  if (draft.value.trim() && draft.value.trim() !== props.todo.title) {
    emit('rename', props.todo.id, draft.value.trim())
  }
}
</script>

<template>
  <li :class="{ done: todo.done }">
    <input type="checkbox" :checked="todo.done" @change="emit('toggle', todo.id)">
    <input v-if="editing" ref="input" v-model="draft" @keyup.enter="finish" @blur="finish">
    <span v-else @dblclick="startEdit">{{ todo.title }}</span>
    <button @click="emit('remove', todo.id)" aria-label="Remove">×</button>
  </li>
</template>
The child never changes props.todo itself. It emits rename and the parent updates its own array, so the list has one owner.

Where to go next

Key takeaways

  • A to-do app exercises the core of Vue: ref state, v-for with :key, v-model, @submit.prevent and computed.
  • Filtered lists and counters are derived state, so compute them instead of storing them.
  • Edit in place by tracking which item is being edited, not by adding an editing flag to every item.
  • Persist with a deep watch and localStorage, guarded by try/catch.
  • Split into parent (owns state) and child (props in, events out) once the template grows.