Hello! It's time to put everything you've learned in this module into practice.
In our previous lessons, we explored the foundational pieces of Vue's reactivity system. You've learned how to:
- Create reactive state with
ref(). - Bind that state to form inputs with
v-model. - Render lists of data with the
v-fordirective. - Handle user events like button clicks.
- Derive new reactive values using
computed.
This lesson is the Mini Project for the "Vue.js: Foundations and Reactivity" module. We're going to synthesize all these concepts to build a classic but essential web application: a to-do list. The goal is to create a simple, single-component app that allows you to add new tasks and see them displayed on the screen.
This hands-on project is the perfect way to solidify your understanding of how Vue's core features work together to create dynamic user interfaces. Let's get building!
Your Goal: A Simple To-Do App
By the end of this lesson, you will have built a functional to-do list application that looks something like this. It will have an input field to add a new task and a list that updates automatically as new tasks are added.

Step 1: Setting Up Your Vue Project
First, we need a fresh Vue project. We'll use Vite, the modern build tool that has become the standard for scaffolding Vue applications.
I've provided a link to a guide that walks you through the creation steps. Follow the instructions to create a new project, install the dependencies, and clean up the default files.
This guide from Djamware provides clear, step-by-step terminal commands for creating a new Vue 3 project with Vite. It's exactly what we need to get our project started quickly.
Follow 'Step 1' through 'Step 4'. When prompted for a project name, you can use something like vue-todo-app. For this project, you can say 'No' to all the optional features like TypeScript, Router, etc. We want to start with the simplest setup. Once you have the project running, replace the entire content of src/App.vue with the basic boilerplate provided in 'Step 4' of the guide.
After completing these steps, you should have a blank white page with the title "Vue 3 Composition API Playground". Your project is now ready.
Step 2: Defining the State and Logic
A to-do app needs to keep track of two main things:
- The text the user is currently typing into the "new task" input field.
- The list of all the tasks that have been added.
We'll manage this state using ref(). We also need a function that adds the new task to our list.
The following video provides an excellent walkthrough of building a task app. We'll watch specific parts to see how to implement this core logic.
Modern Vue.js Crash Course | with TypeScript + script setup + Composition API
The 'Modern Vue.js Crash Course' by Syntax is a fantastic resource that builds a task app from scratch. We'll use it to guide our implementation of the state and form handling.
Watch the following segments and implement the logic in your src/App.vue file: Setting up State (05:12 - 06:05): See how ref is imported and used to create reactive state variables. Create two refs in your <script setup> block: one for the new task input (e.g., newTodo) and one for the list of all tasks (e.g., todos, initialized as an empty array []). Handling Form Input (07:22 - 08:32): This part introduces v-model for two-way data binding on the form's input field. We'll build the template in the next step, but it's good to see how the state and the view will be connected. Adding a Task (14:15 - 15:45): This segment shows how to create the addTask function. This function will push a new object representing the task into your todos array. Pay attention to how .value is used to access and modify the ref data.
After watching, your <script setup> block in App.vue should look similar to this:
import { ref } from 'vue'
// State for the new todo input
const newTodo = ref('')
// State for the list of all todos
const todos = ref([])
// Function to add a new todo
function addTodo() {
// We'll add some validation to not add empty todos
if (newTodo.value.trim() !== '') {
todos.value.push({
id: Date.now(), // A simple way to get a unique ID
text: newTodo.value,
done: false
})
// Clear the input field after adding
newTodo.value = ''
}
}
Step 3: Building the Template (UI)
Now that our logic is ready, let's create the HTML structure in the <template> section of App.vue. We need a form for adding tasks and a list to display them.
-
The Form: Create a
<form>element that contains an<input>for text and a<button>to submit.- Use the
@submit.prevent="addTodo"directive on the<form>tag. This tells Vue to call ouraddTodofunction when the form is submitted and to prevent the default browser behavior of reloading the page. - Use the
v-model="newTodo"directive on the<input>tag to create a two-way binding with ournewTodostate.
- Use the
-
The List: Create a
<ul>element to display the tasks.- Use the
v-for="todo in todos"directive on an<li>element to loop through ourtodosarray. - Crucially, add a
:key="todo.id"binding. Vue uses thekeyto track each item in the list efficiently. It's essential for performance and to avoid bugs when the list changes. - Inside the
<li>, display the task's text using{{ todo.text }}.
- Use the
The video you just watched also covers this. If you need a refresher, re-watch the parts that show the template being built.
Final Code and Review
Once you've implemented the logic and template, your complete App.vue file should look like this. Compare it with your own code to ensure everything is correct.
Click to see the complete `App.vue` code
<script setup>
import { ref } from 'vue'
// State for the new todo input
const newTodo = ref('')
// State for the list of all todos
const todos = ref([
// You can add some initial todos to test the display
{ id: 1, text: 'Learn Vue.js', done: false },
{ id: 2, text: 'Build a to-do app', done: false },
])
// Function to add a new todo
function addTodo() {
if (newTodo.value.trim() !== '') {
todos.value.push({
id: Date.now(),
text: newTodo.value,
done: false
})
newTodo.value = ''
}
}
</script>
<template>
<main>
<h1>My To-Do List</h1>
<form @submit.prevent="addTodo">
<input
v-model="newTodo"
placeholder="e.g., Finish the project"
/>
<button>Add Task</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
<!-- A conditional message for when there are no todos -->
<p v-if="todos.length === 0">
You have no tasks yet. Add one above!
</p>
</main>
</template>
<style>
/* Some basic styling to make it look nice */
main {
max-width: 500px;
margin: 2rem auto;
font-family: sans-serif;
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f9f9f9;
}
h1 {
text-align: center;
color: #2c3e50;
}
form {
display: flex;
margin-bottom: 1rem;
}
input {
flex-grow: 1;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px 0 0 4px;
}
button {
padding: 0.5rem 1rem;
font-size: 1rem;
border: 1px solid #42b983;
background-color: #42b983;
color: white;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
button:hover {
background-color: #36a374;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 0.75rem;
margin-bottom: 0.5rem;
background-color: white;
border: 1px solid #eee;
border-radius: 4px;
}
</style>
Run your development server (npm run dev) and open the app in your browser. You should be able to type a task, click "Add Task", and see it instantly appear in the list below. Congratulations, you've built your first interactive Vue application!
Conclusion
Fantastic work! You've successfully built a complete, albeit simple, application from scratch. This project reinforced how Vue's core features work in harmony.
Key Takeaways:
- Synthesis: We combined
ref,v-model,v-for, and event listeners (@submit) to create a single, cohesive application. - State-Driven UI: You didn't manually touch the DOM. You simply changed your JavaScript data (the
todosarray), and Vue's reactivity system automatically updated the UI to match. This is the central idea behind modern frameworks like Vue. - Project Foundation: You now have a solid base project that we can build upon in the upcoming lessons.
In the next lesson, we will enhance this to-do app by adding functionality to delete tasks and mark them as complete. More importantly, we'll begin to refactor our app into smaller, reusable components—a crucial skill for building scalable applications.