Chapter 21: Vue Components
Vue Components, the single most important building block in Vue. Once you really understand components, everything else in Vue (routing, state, forms, styling) suddenly makes a lot more sense.
Think of a Vue component as a reusable, self-contained Lego piece of your user interface:
- It has its own HTML structure (template)
- It has its own JavaScript logic (state, methods, computed…)
- It has its own CSS styles (scoped so they don’t leak)
- It can receive data from parents (props)
- It can send events up to parents (emits)
- It can be used anywhere — like <MyButton />, <TodoItem />, <UserCard />
In Vue 3 (2026 standard), almost everything you see on screen is a component — even <div>, <button>, <input> are technically native HTML elements, but we build our UI by composing our own custom components.
Two Main Flavors in Vue 3 (you need to know both)
- Options API (classic style — still very common in old projects & tutorials)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<script> export default { name: 'GreetingCard', props: ['name'], data() { return { count: 0 } }, methods: { increment() { this.count++ } } } </script> |
- Composition API + <script setup> (modern, recommended since ~2022 — 90%+ of new code in 2026)
|
0 1 2 3 4 5 6 7 8 9 10 |
<script setup> defineProps(['name']) const count = ref(0) function increment() { count.value++ } </script> |
From today onward — we will use <script setup> (the future-proof way).
Real Example: A Classic First Component → TodoItem.vue
Let’s build a realistic, reusable todo item component.
File: src/components/TodoItem.vue
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<template> <div class="todo-item" :class="{ completed: todo.done }"> <input type="checkbox" :checked="todo.done" @change="$emit('toggle', todo.id)" /> <span class="text">{{ todo.text }}</span> <button class="delete-btn" @click="$emit('delete', todo.id)"> × </button> </div> </template> <script setup lang="ts"> import { defineProps, defineEmits } from 'vue' const props = defineProps<{ todo: { id: number text: string done: boolean } }>() const emit = defineEmits<{ (e: 'toggle', id: number): void (e: 'delete', id: number): void }>() </script> <style scoped> .todo-item { display: flex; align-items: center; padding: 0.8rem 1rem; background: #f8fafc; border-radius: 8px; margin-bottom: 0.5rem; transition: all 0.2s; } .todo-item:hover { background: #f1f5f9; } .completed .text { text-decoration: line-through; color: #94a3b8; opacity: 0.8; } .text { flex: 1; margin: 0 1rem; font-size: 1.1rem; } .delete-btn { background: none; border: none; color: #ef4444; font-size: 1.4rem; cursor: pointer; padding: 0 0.5rem; opacity: 0.7; } .delete-btn:hover { opacity: 1; } </style> |
How to Use This Component (Parent → Child)
File: src/views/TodoList.vue (a page that uses many TodoItem components)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<template> <div class="todo-list"> <h2>My Todos</h2> <div v-if="todos.length === 0" class="empty"> No tasks yet — add one! </div> <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" @toggle="toggleTodo" @delete="deleteTodo" /> <div class="add-form"> <input v-model="newTodo" placeholder="What needs to be done?" @keyup.enter="addTodo" /> <button @click="addTodo">Add</button> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import TodoItem from '@/components/TodoItem.vue' interface Todo { id: number text: string done: boolean } const todos = ref<Todo[]>([ { id: 1, text: 'Learn Vue components', done: true }, { id: 2, text: 'Build something real', done: false } ]) const newTodo = ref('') function addTodo() { if (!newTodo.value.trim()) return todos.value.push({ id: Date.now(), text: newTodo.value.trim(), done: false }) newTodo.value = '' } function toggleTodo(id: number) { const todo = todos.value.find(t => t.id === id) if (todo) todo.done = !todo.done } function deleteTodo(id: number) { todos.value = todos.value.filter(t => t.id !== id) } </script> <style scoped> .todo-list { max-width: 600px; margin: 2rem auto; padding: 1rem; } .add-form { display: flex; gap: 0.8rem; margin-top: 1.5rem; } input { flex: 1; padding: 0.8rem; border-radius: 6px; border: 1px solid #d1d5db; } button { padding: 0.8rem 1.5rem; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; } .empty { text-align: center; color: #6b7280; padding: 2rem; font-style: italic; } </style> |
Quick Summary Table – Core Component Concepts
| Concept | What it is | Syntax (script setup) | Example from above |
|---|---|---|---|
| Template | HTML-like UI structure | <template>…</template> | <div class=”todo-item”>…</div> |
| Props | Data passed from parent → child | defineProps<{ todo: Todo }>() | :todo=”todo” |
| Emits | Events sent from child → parent | defineEmits<{ (e: ‘toggle’, id: number): void }>() | @toggle=”toggleTodo” |
| State | Internal reactive data | const count = ref(0) | (not in child, but in parent) |
| Scoped Styles | CSS only for this component | <style scoped>…</style> | .todo-item { … } |
| Composition API | Modern way to write logic | <script setup> | Whole <script setup> block |
Best Practices You Just Learned (2026 style)
- Always use :key in v-for (we used :key=”todo.id”)
- Use TypeScript interfaces for props (makes large apps much safer)
- Prefer defineProps / defineEmits without extra props: / emits: arrays
- Keep child components dumb & reusable — they receive props & emit events
- Parent owns the state → child just displays & notifies
Your Next Mini Homework
- Create TodoItem.vue exactly as above
- Create TodoList.vue (or put it in HomeView.vue)
- Add one more feature: a button in TodoItem that emits ‘edit’
- In parent → when ‘edit’ → prompt for new text and update
Any part still fuzzy? Want me to show:
- How to make TodoItem editable inline?
- Global component registration vs local imports?
- Props validation / default values?
- Slots (for customizable content inside component)?
Just say — we’ll build the next piece together 🚀
Happy component composing from Hyderabad! 💙
