Lesson 6 of 7
Mini Project: Todo App with Pinia
You will build a todo app with Vue 3 and Pinia, Vue's official state management . It brings together components, reactivity and shared state.
// stores/todos.js (install pinia: npm install pinia)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTodosStore = defineStore('todos', () => {
// State
const todos = ref([])
const filter = ref('all') // 'all' | 'active' | 'done'
// Getters (computed)
const filtered = computed(() => {
if (filter.value === 'active') return todos.value.filter(t => !t.done)
if (filter.value === 'done') return todos.value.filter(t => t.done)
return todos.value
})
const remaining = computed(() => todos.value.filter(t => !t.done).length)
// Actions
function add(text) {
todos.value.push({ id: Date.now(), text, done: false })
}
function toggle(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
function remove(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
function clearDone() {
todos.value = todos.value.filter(t => !t.done)
}
return { todos, filter, filtered, remaining, add, toggle, remove, clearDone }
})<!-- App.vue -->
<template>
<div class="app">
<h1>Todos <span class="badge">{{ store.remaining }}</span></h1>
<!-- Add new todo -->
<form @submit.prevent="addTodo">
<input v-model="newTodo" placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<!-- Filter buttons -->
<div class="filters">
<button v-for="f in filters" :key="f"
:class="{ active: store.filter === f }"
@click="store.filter = f">
{{ f }}
</button>
</div>
<!-- Todo list -->
<TransitionGroup name="list" tag="ul">
<li v-for="todo in store.filtered" :key="todo.id" :class="{ done: todo.done }">
<input type="checkbox" :checked="todo.done" @change="store.toggle(todo.id)" />
<span>{{ todo.text }}</span>
<button @click="store.remove(todo.id)">✕</button>
</li>
</TransitionGroup>
<button v-if="store.todos.some(t => t.done)" @click="store.clearDone">
Clear completed
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useTodosStore } from './stores/todos'
const store = useTodosStore()
const newTodo = ref("")
const filters = ['all', 'active', 'done']
function addTodo() {
if (!newTodo.value.trim()) return
store.add(newTodo.value.trim())
newTodo.value = ""
}
</script>
<style>
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(-30px); }
</style>Finished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.