Vue, JavaScript, Tutorials Vue 3 Components: Building Reusable UI Blocks
6 min read
6 viewsVue 3 Components: Building Reusable UI Blocks
Part 4 of the Vue 3 Fundamentals series — understanding SFCs, props, and component events.
Every Vue application is a tree of components.
At the root, you have App.vue. Underneath it, you have everything else — your header, your search bar, your individual weather cards. Each is a self-contained piece of UI with its own logic, template, and styles.
This is the component model. It’s how Vue (and every modern framework) manages complexity. Instead of one massive HTML file with thousands of lines of JavaScript, you break your UI into small, focused, reusable pieces.
In this article, we’ll look at what components are, how to build them, and how to make them talk to each other — using our weather dashboard as the domain throughout.
What Is a Component?
A Vue component is a reusable, self-contained unit of UI. It encapsulates three things:
- Template — the HTML structure
- Script — the logic (data, methods, computed values)
- Style — the CSS that applies to it
In Vue 3, we write components as Single-File Components (SFCs) — .vue files that contain all three in one place:
<!-- src/components/WeatherCard.vue -->
<script setup>
const conditionEmoji = {
Sunny: '☀️',
Cloudy: '☁️',
Rainy: '🌧️',
Windy: '💨',
Snowy: '❄️',
}
</script>
<template>
<div class="card">
<h2>Atlanta, GA</h2>
<p class="temp">72°F</p>
<p class="condition">{{ conditionEmoji['Sunny'] }} Sunny</p>
</div>
</template>
<style scoped>
.card {
padding: 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: white;
}
.temp { font-size: 2.5rem; font-weight: bold; margin: 0.25rem 0; }
.condition { color: #64748b; }
</style>
That’s a complete component. No external dependencies. Drop it anywhere in your app and it works.
Using a Component
To use WeatherCard in App.vue, import it and place it in the template:
<!-- src/App.vue -->
<script setup>
import WeatherCard from './components/WeatherCard.vue'
</script>
<template>
<main>
<h1>🌍 Weather Dashboard</h1>
<WeatherCard />
</main>
</template>
With <script setup>, imported components are automatically available in the template — no registration step needed.
Notice the <WeatherCard /> syntax — Vue components use PascalCase in templates, which visually distinguishes them from native HTML elements like <div> and <input>.
Props: Passing Data Into a Component
A component that always shows the same city and temperature isn’t very useful. Props let a parent pass data into a child.
Let’s update WeatherCard to accept real city data:
<!-- src/components/WeatherCard.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
city: {
type: Object,
required: true
},
unit: {
type: String,
default: 'F'
}
})
const conditionEmoji = {
Sunny: '☀️', Cloudy: '☁️', Rainy: '🌧️', Windy: '💨', Snowy: '❄️',
}
const displayTemp = computed(() =>
props.unit === 'C'
? Math.round((props.city.temp - 32) * 5 / 9)
: props.city.temp
)
</script>
<template>
<div class="card">
<h2>{{ city.name }}</h2>
<p class="temp">{{ displayTemp }}°{{ unit }}</p>
<p class="condition">
{{ conditionEmoji[city.condition] ?? '🌡️' }}
{{ city.condition }}
</p>
<p class="meta">💧 {{ city.humidity }}% · 💨 {{ city.wind }} mph</p>
</div>
</template>
defineProps is a Vue macro — you don’t import it, it’s built into <script setup>. It declares what data the component expects to receive.
Now the parent passes city data as attributes:
<WeatherCard
:city="{ id: 1, name: 'Atlanta, GA', temp: 72, condition: 'Sunny', humidity: 45, wind: 8 }"
unit="F"
/>
Props flow downward — from parent to child. A child component should never directly modify the props it receives. If you need to signal a change back, the child communicates upward through events.
Events: Sending Data Back Up
When something happens inside a child — a button click, a user removing a city — the child emits an event and the parent listens for it.
Let’s add a “Remove” button to WeatherCard:
<!-- src/components/WeatherCard.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
city: { type: Object, required: true },
unit: { type: String, default: 'F' }
})
const emit = defineEmits(['remove'])
const conditionEmoji = { Sunny: '☀️', Cloudy: '☁️', Rainy: '🌧️', Windy: '💨', Snowy: '❄️' }
const displayTemp = computed(() =>
props.unit === 'C'
? Math.round((props.city.temp - 32) * 5 / 9)
: props.city.temp
)
</script>
<template>
<div class="card">
<header>
<h2>{{ city.name }}</h2>
<button class="remove-btn" @click="emit('remove', city.id)">✕</button>
</header>
<p class="temp">{{ displayTemp }}°{{ unit }}</p>
<p class="condition">{{ conditionEmoji[city.condition] ?? '🌡️' }} {{ city.condition }}</p>
<p class="meta">💧 {{ city.humidity }}% · 💨 {{ city.wind }} mph</p>
</div>
</template>
defineEmits declares what events this component can fire. When the remove button is clicked, we emit 'remove' with the city’s id as the payload.
The parent listens with @event-name:
<!-- src/App.vue -->
<script setup>
import { ref } from 'vue'
import WeatherCard from './components/WeatherCard.vue'
const cities = ref([
{ id: 1, name: 'Atlanta, GA', temp: 72, condition: 'Sunny', humidity: 45, wind: 8 },
{ id: 2, name: 'New York, NY', temp: 61, condition: 'Cloudy', humidity: 60, wind: 12 },
])
const unit = ref('F')
function removeCity(id) {
cities.value = cities.value.filter(c => c.id !== id)
}
</script>
<template>
<main>
<WeatherCard
v-for="city in cities"
:key="city.id"
:city="city"
:unit="unit"
@remove="removeCity"
/>
</main>
</template>
This is the props down, events up pattern. The parent owns the data. The child only reports what happened. Data flow stays predictable and components stay decoupled.
Scoped Styles
The scoped attribute on <style> means the styles in a component only apply to that component’s HTML:
<style scoped>
/* This .card class won't affect any other .card elements in the app */
.card {
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
border-radius: 12px;
}
</style>
Vue achieves this by adding a unique data attribute to the component’s elements at compile time. No naming collisions, no specificity battles between components.
Component Organization
As your app grows, keep your components folder organized by responsibility:
src/
└── components/
├── WeatherCard.vue → Individual city weather card
├── WeatherSearch.vue → City search input + add button
└── UnitToggle.vue → °F / °C switcher
Each component has one clear job. WeatherCard displays weather for one city. WeatherSearch handles the search/add flow. UnitToggle switches the temperature unit. None of them need to know what the others are doing.
That separation makes each component independently testable — and means you can update one without risking breaking the others.
The Mental Model
- SFC → one
.vuefile = template + script + scoped styles. defineProps→ declare what data flows in from the parent.defineEmits→ declare what events flow out to the parent.- Props down, events up → the rule that keeps data flow predictable.
- Scoped styles → CSS that stays in its lane.
Your Turn
Try these before moving to Article 5:
- Create a
UnitToggle.vuecomponent that emits a'toggle'event when clicked. - In
App.vue, listen to@toggleand flip theunitref between'F'and'C'. - Pass the updated
unitas a prop to eachWeatherCardand watch all the temperatures update at once.
If all three work — you’ve just implemented the entire props-down events-up pattern on a real feature.
What’s Next
In Article 5, we’ll cover the most important directives you’ll use every day: v-if, v-for, v-bind, v-model, and v-on.
Progress over perfection. Let’s go.
This is Part 4 of the Vue 3 Fundamentals series. Sources: Vue.js Component Basics, Vue School — Vue Component Fundamentals with the Composition API.