Split Components

Componentization is the core design philosophy of Vue.

In this chapter, you will learn how to split a page into reusable small components and achieve inter-component communication through props and emit.


Why Split Components?

As features grow, a file gets longer and longer, and hundreds of lines of code crammed into one .vue file become difficult to maintain.

The purpose of splitting components:Each component does only one thing, and together they accomplish complex functionality.。

What Granularity Is Appropriate?

A good rule of thumb is:When a code block has its own data, its own behavior, and can be reused elsewhere,it should become an independent component.

Page AreaWhether to Split into a ComponentReason
Navigation BarYesNeeded on every page; independent functionality
Article CardYesEvery article in the list shares the same structure
Category Filter Button GroupYesHas its own state and interactions
Simple Wrapper ContainernoOnly for layout, no independent behavior

Parent to Child: defineProps

The parent component passes data to the child component,propsimplemented through defineProps.

You can think of it as a "function parameter" — when the parent component calls the child component, it passes data in as a parameter.

Example

<!-- File path: src/components/BlogCard.vue -->
<script setup>
// Declare the props that this component receives
// defineProps is a compiler macro, no import needed
const props = defineProps({
title: String, // Article title, string type
summary: String, // Article summary
date: String, // Publication date
category: String // Category tag
})

// It can also be written in array form (without type checking):
// const props = defineProps(['title', 'summary', 'date', 'category'])
</script>

<template>
  <div class="card">
    <span class="tag">{{ category }}</span>
    <h3>{{ title }}</h3>
    <p>{{ summary }}</p>
    <span class="date">{{ date }}</span>
  </div>
</template>

<style scoped>
.card {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.tag {
  display: inline-block;
  padding: 2px 10px;
  background: #E8EEF5;
  color: #427DB8;
  border-radius: 12px;
  font-size: 12px;
}
.card h3 { margin: 10px 0 8px; font-size: 18px; }
.card p { color: #666; font-size: 14px; line-height: 1.6; }
.date { font-size: 12px; color: #999; }
</style>

When the parent component uses BlogCard, it passes data in by binding attributes with a colon:

Example

<!-- File path: src/views/HomeView.vue -->
<script setup>
import { ref } from 'vue'
import BlogCard from '../components/BlogCard.vue'

const articles = ref([
{ id: 1, title: 'Vue3 入门', summary: 'fromzero学 Vue3', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Promise in Detail', summary: 'Understand Async', date: '2024-05-08', category: 'JavaScript' },
])
</script>

<template>
  <div class="article-grid">
    <!-- The parent component passes data to the child component via :prop-name="value" -->
    <BlogCard
     v-for="article in articles"
     :key="article.id"
     :title="article.title"
     :summary="article.summary"
     :date="article.date"
     :category="article.category"
   />
  </div>
</template>

props area one-way data flow: data can only flow parent → child; the child component cannot modify the value of props. If the child component needs to modify it, it should notify the parent via emit to make the change.


Child to Parent: defineEmits

When a child component needs to notify the parent component that something happened (e.g., "I was clicked"), it does so byemitemitting an event.

You can think of it as a "callback function" — the child component triggers an event, and the parent component listens and responds.

Example

<!-- File path: src/components/CategoryFilter.vue -->
<script setup>
const props = defineProps({
categories: Array, // List of all categories
activeCategory: String // Currently selected category
})

// Declare the events to be emitted
const emit = defineEmits(['update-category'])

function selectCategory(cat) {
// Trigger the event, passing the selected category to the parent component
  emit('update-category', cat)
}
</script>

<template>
  <div class="filter-bar">
    <button
     v-for="cat in categories"
     :key="cat"
     :class="{ active: activeCategory === cat }"
     @click="selectCategory(cat)"
   >
      {{ cat }}
    </button>
  </div>
</template>

The parent component uses@eventName="handlerFunction"to listen for events from the child component:

Example

<!-- File path: src/views/HomeView.vue -->
<script setup>
import { ref, computed } from 'vue'
import CategoryFilter from '../components/CategoryFilter.vue'

const activeCategory = ref('All')

const categories = ['All', 'Vue', 'JavaScript', 'CSS']

// Respond to the update-category event emitted by the child component
function handleCategoryChange(cat) {
  activeCategory.value = cat
}
</script>

<template>
  <!-- @update-category="handleCategoryChange" -->
  <CategoryFilter
   :categories="categories"
   :active-category="activeCategory"
   @update-category="handleCategoryChange"
 />
</template>

Complete Data Flow

The parent component, throughprops, passes data downward;the child component, throughemit,sends events upward.

Data is always "owned" and modified by the parent component; the child component is only responsible for rendering and notifying.


Hands-on: Split Out Three Components

The blog project needs three components, with the directory structure as follows:

src/
├── components/
│   ├── NavBar.vue          # 顶部导航栏
│   ├── BlogCard.vue        # 单篇文章卡片
│   └── CategoryFilter.vue  # 分类筛选按钮组
├── views/
│   └── HomeView.vue        # 首页(父组件)
└── App.vue

NavBar.vue — Navigation Bar

Example

<!-- File path: src/components/NavBar.vue -->
<script setup>
// The navigation bar only needs to display for now; search and dark mode toggle will be added later
</script>

<template>
  <header class="navbar">
    <a href="/" class="logo">EXAMPLE Blog</a>
    <nav>
      <a href="/">Home</a>
      <a href="#">About</a>
    </nav>
  </header>
</template>

<style scoped>
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 40px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.logo {
  font-size: 22px;
  font-weight: bold;
  color: #427DB8;
  text-decoration: none;
}

nav a {
  margin-left: 24px;
  text-decoration: none;
  color: #333;
  font-size: 15px;
}

nav a:hover {
  color: #427DB8;
}
</style>

HomeView.vue — Integrating All Components

Example

<!-- File path: src/views/HomeView.vue -->
<script setup>
import { ref, computed } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'

// Article data
const articles = ref([
{ id: 1, title: 'Vue3 入门完全Guide', summary: 'fromzerostart学习 Vue3 combinationformula API', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'JS Asynchronous Programming In Detail', summary: Understand Promise and async/await, date: '2024-05-08', category: 'JavaScript' },
{ id: 3, title: Practical CSS Grid Layout, summary: 'Implement Responsive Layout with Grid', date: '2024-05-05', category: 'CSS' },
{ id: 4, title: 'Vue3 responseformulaPrinciple', summary: '深入Understanding ref and reactive', date: '2024-05-03', category: 'Vue' },
{ id: 5, title: 'The Complete Guide to Flexbox', summary: 'Learn Flex Layout in One Article', date: '2024-05-01', category: 'CSS' },
])

// Extract all categories (deduplicate)
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
return ['All', ...new Set(cats)]
})

// The currently selected category
const activeCategory = ref('All')

// Filter articles by category
const filteredArticles = computed(() => {
if (activeCategory.value === 'All') return articles.value
  return articles.value.filter(a => a.category === activeCategory.value)
})

// Handle the category switch event emitted by the child component
function handleCategoryChange(cat) {
  activeCategory.value = cat
}
</script>

<template>
  <div class="home">
    <h2 class="section-title">Latest Articles</h2>

    <!-- Category filter component -->
    <CategoryFilter
     :categories="categories"
     :active-category="activeCategory"
     @update-category="handleCategoryChange"
   />

    <!-- Empty state -->
    <p v-if="filteredArticles.length === 0" class="empty-tip">
No articles in this category yet.
    </p>

    <!-- Article card list -->
    <div v-else class="article-grid">
      <BlogCard
       v-for="article in filteredArticles"
       :key="article.id"
       :title="article.title"
       :summary="article.summary"
       :date="article.date"
       :category="article.category"
     />
    </div>
  </div>
</template>

<style scoped>
.home { max-width: 960px; margin: 0 auto; padding: 20px; }
.section-title { font-size: 24px; margin-bottom: 20px; }
.article-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
  margin-top: 20px;
}
.empty-tip { text-align: center; color: #999; padding: 60px 0; }
</style>

Chapter Summary

In this chapter, you mastered the three core concepts of componentization: why to split components, using props (defineProps) for parent-to-child communication, and using emit (defineEmits) for child-to-parent communication.

The blog project's three components — NavBar, BlogCard, and CategoryFilter — are now in place, and the code structure is much clearer than the mess in Chapter 1.

Other Extensions