# Building a TODO app with the Vue 3 Composition API and localStorage

> A walkthrough of a Vue 3 TODO app built with ref, computed and onMounted: reactive task state, To-Do and Done tabs, UUID task IDs, confirmation flows and localStorage persistence.

## What does the app do?

It's a small task manager built with **Vue 3** and the **Composition API**. You can:

- **Create** tasks.
- **Complete** a task, which moves it from *To-Do* to *Done*.
- **Restore** a completed task back to *To-Do*.
- **Delete** a task.
- Switch between **To-Do** and **Done** with a reactive tab interface.

Tasks are stored in the browser's `localStorage`, so they are still there after a reload. Destructive or status-changing actions show a confirmation first, so a misclick doesn't lose your work.

<project-embed src="https://codepen.io/ps_cristopher/embed/PojRQOB?default-tab=js%2Cresult&theme-id=dark" title="Vue 3 TODO app built with the Composition API">

The [live app on CodePen](https://codepen.io/ps_cristopher/pen/PojRQOB). Switch to the JS tab to read the full source.

</project-embed>

## How is the state modeled?

All the state hangs off one reactive array. Each task is a plain object with a unique `id`, a `title` and a `done` flag. The tab views are **derived** from that array with `computed` instead of being stored separately:

```js
import { ref, computed } from 'vue'

const tasks = ref([])
const activeTab = ref('todo')

const todoTasks = computed(() => tasks.value.filter(task => !task.done))
const doneTasks = computed(() => tasks.value.filter(task => task.done))

const visibleTasks = computed(() =>
  activeTab.value === 'todo' ? todoTasks.value : doneTasks.value
)
```

*Simplified for this article. See the CodePen for the exact implementation.*

Keeping one source of truth and computing the views from it means the lists can't drift out of sync. Completing a task only flips one flag, and both tabs update automatically.

## Why give each task a UUID?

Using the array index as an identifier breaks as soon as items are deleted or reordered. Giving each task a **UUID** when it is created provides:

- a stable `:key` for `v-for`, so Vue updates the DOM correctly,
- a reliable way to find the task to complete, restore or delete.

```js
const addTask = (title) => {
  tasks.value.push({ id: uuid(), title, done: false })
}

const toggleTask = (id) => {
  const task = tasks.value.find(task => task.id === id)
  if (task) task.done = !task.done
}

const removeTask = (id) => {
  tasks.value = tasks.value.filter(task => task.id !== id)
}
```

## How does persistence with localStorage work?

`localStorage` only stores strings, so the task list is serialized with `JSON.stringify` when it is saved and parsed back with `JSON.parse` when it is loaded. Loading happens in `onMounted`, once the app is running in the browser:

```js
import { onMounted } from 'vue'

const STORAGE_KEY = 'tasks'

const saveTasks = () => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks.value))
}

onMounted(() => {
  const saved = localStorage.getItem(STORAGE_KEY)
  if (saved) tasks.value = JSON.parse(saved)
})
```

Saving after every change keeps `localStorage` and the UI in sync, with no backend at all.

## Why confirm destructive actions?

Deleting a task or changing its status are the two actions a user is most likely to regret. Asking for confirmation before running them is cheap and saves frustration, and it's a habit worth keeping in larger apps.

## What frontend concepts does it demonstrate?

Small as it is, the project covers concepts that come up in any production Vue app:

- **Reactive state** with `ref`.
- **Derived state** with `computed`, using one source of truth.
- **Lifecycle hooks** (`onMounted`) for browser-only work.
- **Client-side persistence** with the Web Storage API.
- **Stable identity** for list items using UUIDs.
- **Event-driven UI** with confirmation flows.

<style>

html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}

</style>

---

- [View on CodePen](https://codepen.io/ps_cristopher/pen/PojRQOB)
