Components and Layouts
Charlie's team consists of five developers, and each one writes components differently—some import them manually, others register them globally, and there's no consistent naming convention. Alice noticed that the page was loading a lot of unnecessary components, which was slowing down the first-screen load. Bob wanted to use a different layout for the admin dashboard but didn't know how to switch to it.
1. What You'll Learn
- Automatic Component Import: Directory Scanning Rules and Naming Conventions
- Layouts System: Switching between the default layout and custom layouts
- Component Lazy Loading: The "Lazy" Prefix and Suspense
- Best Practices for Component Props and Emits
- MegaShop Header/Footer/ProductCard: Hands-On Guide
2. A True Story About a Team
(1) Pain Point: Disorganized Component Management
Charlie's team of five works in silos: Alice manually imports each component, Bob uses global registration, which causes the package size to balloon, and the new colleague can't figure out what the components are called. There are three different ways to reference a single ProductCard across different files.
(2) Solution for Nuxt 3's Automatic Import
Nuxt 3 automatically scans the components/ directory, generates component names according to the rules, and ensures everyone references them in a consistent manner:
<!-- No import needed, just use it -->
<ProductCard :product="item" />
(3) Benefits: Standardization + Reduced Size
The team standardized the way components are referenced; the "Lazy" prefix ensures that only visible components are loaded on the first screen, reducing the size of MegaShop's first-screen JavaScript by 40%.
3. Automatic Component Import
(1) Directory Scanning and Naming Conventions
graph TB
A[components/] --> B[Root level]
A --> C[Sub-directory]
A --> D[Nested directory]
B --> E[ProductCard.vue → ProductCard]
C --> F[product/Card.vue → ProductCard ①]
D --> G[shop/product/Card.vue → ShopProductCard]
style G fill:#ffe,stroke:#f90
pathPrefix: false, product/Card.vue → Card may result in a naming conflict. We recommend keeping the default prefix.
(2) Comparison of Naming Conventions
| File Path | Default Component Name | pathPrefix: false |
|---|---|---|
| components/AppHeader.vue | AppHeader | AppHeader |
| components/product/Card.vue | ProductCard | Card |
| components/shop/ProductCard.vue | ShopProductCard | ProductCard |
| components/admin/user/Table.vue | AdminUserTable | Table |
(1) ▶ Example: Automatic Component Import
<!-- components/ProductCard.vue -->
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>${{ product.price }} USD</p>
<button @click="$emit('add-to-cart', product)">Add to Cart</button>
</div>
</template>
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
}
defineProps<{ product: Product }>()
defineEmits<{ 'add-to-cart': [product: Product] }>()
</script>
Output:
// Execution Successful
(2) ▶ Example: Using the Auto-Import Component on a Page
<!-- pages/products/index.vue -->
<template>
<div>
<h1>All Products</h1>
<div class="grid">
<!-- ProductCard auto-imported -->
<ProductCard
v-for="p in products"
:key="p.id"
:product="p"
@add-to-cart="handleAdd"
/>
</div>
</div>
</template>
<script setup lang="ts">
const { data: products } = await useFetch('/api/products')
function handleAdd(product: any) {
console.log('Added:', product.name)
}
</script>
Output:
// Execution Successful
4. Layouts Layout System
(1) How Layout Works
graph TB
A[app.vue] --> B[NuxtLayout]
B --> C{Current Layout}
C -->|default| D[layouts/default.vue]
C -->|sidebar| E[layouts/sidebar.vue]
D --> F[NuxtPage - Page Content]
E --> F
(2) The Relationship Between app.vue and Layout
<!-- app.vue - Root component with layout support -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
(1) ▶ Example: Default Layout
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main class="content">
<!-- Page content renders here -->
<slot />
</main>
<AppFooter />
</div>
</template>
Output:
// Execution Successful
(2) ▶ Example: Sidebar Layout
<!-- layouts/sidebar.vue -->
<template>
<div class="layout-sidebar">
<AppHeader />
<div class="main-wrapper">
<aside class="sidebar">
<nav>
<NuxtLink to="/admin">Dashboard</NuxtLink>
<NuxtLink to="/admin/products">Products</NuxtLink>
<NuxtLink to="/admin/orders">Orders</NuxtLink>
</nav>
</aside>
<main class="content">
<slot />
</main>
</div>
</div>
</template>
Output:
// Execution Successful
(3) ▶ Example: Page-Switching Layout
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>Admin Dashboard</h1>
<p>Welcome, Bob</p>
</div>
</template>
<script setup lang="ts">
// Switch to sidebar layout for this page
definePageMeta({
layout: 'sidebar'
})
</script>
Output:
// Execution Successful
(3) Comparison of Layout vs. Nested Routes
| Dimension | Layout | Nested Routing |
|---|---|---|
| Definition Location | layouts/*.vue | pages/parent.vue |
| Content Slot | <slot /> |
<NuxtPage /> |
| Switching Method | definePageMeta | Automatic (Routing Hierarchy) |
| Data Retrieval | Not Recommended | Each Gets What They Need |
| Use Cases | Overall Page Layout | Shared UI Across Routing Levels |
5. Lazy Loading of Components
(1) Lazy Loading Mechanism
Nuxt 3 automatically registers a "Lazy" version of each component, loading the code only when the component is rendered for the first time.
(1) ▶ Example: Lazy prefix lazy loading
<template>
<div>
<!-- Eagerly loaded - included in initial bundle -->
<AppHeader />
<!-- Lazy loaded - code-split, loaded on first render -->
<LazyProductCard
v-for="p in products"
:key="p.id"
:product="p"
/>
<!-- Lazy with v-if - only loaded when condition is true -->
<LazyProductReviewModal
v-if="showReviewModal"
:product-id="selectedProductId"
@close="showReviewModal = false"
/>
</div>
</template>
Output:
// Execution Successful
(2) ▶ Example: Asynchronous Loading with Suspense
<template>
<div>
<Suspense>
<template #default>
<LazyHeavyChart :data="chartData" />
</template>
<template #fallback>
<div class="loading">Loading chart...</div>
</template>
</Suspense>
</div>
</template>
Output:
// Execution Successful
(2) Comparison of Lazy-Loading Strategies
| Strategy | Implementation | Loading Timing | Applicable Scenarios |
|---|---|---|---|
| Instant Load | <ProductCard /> |
When the page loads | Core components on the first screen |
| Lazy prefix | <LazyProductCard /> |
On first render | Components below the fold |
| v-if + Lazy | <LazyModal v-if="show" /> |
When the condition is true | Pop-up/Drawer |
| Suspense | <Suspense> Package |
When asynchronous operation is ready | A "loading" state is required |
6. Best Practices for Props and Emits
(1) ▶ Example: Type-Safe Props and Emits
<!-- components/ProductCard.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
inStock: boolean
}
// Type-safe props with default values
const props = withDefaults(
defineProps<{
product: Product
showStock?: boolean
currency?: string
}>(),
{
showStock: true,
currency: 'USD'
}
)
// Type-safe emits
const emit = defineEmits<{
'add-to-cart': [product: Product]
'toggle-wishlist': [productId: number]
}>()
</script>
<template>
<div class="card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>{{ product.price }} {{ currency }}</p>
<p v-if="showStock">
{{ product.inStock ? 'In Stock' : 'Out of Stock' }}
</p>
<button @click="emit('add-to-cart', product)">Add to Cart</button>
<button @click="emit('toggle-wishlist', product.id)">Wishlist</button>
</div>
</template>
Output:
// Execution Successful
7. Comprehensive Example: MegaShop Layout Component System
<!-- components/AppHeader.vue -->
<template>
<header class="app-header">
<NuxtLink to="/" class="logo">MegaShop</NuxtLink>
<nav>
<NuxtLink to="/products">Products</NuxtLink>
<NuxtLink to="/categories">Categories</NuxtLink>
</nav>
<div class="user-area">
<NuxtLink to="/cart">Cart ({{ cartCount }})</NuxtLink>
<NuxtLink v-if="isLoggedIn" to="/admin">Admin</NuxtLink>
<NuxtLink v-else to="/login">Sign In</NuxtLink>
</div>
</header>
</template>
<script setup lang="ts">
const cart = useState<any[]>('cart', () => [])
const cartCount = computed(() => cart.value.length)
const isLoggedIn = useState('isLoggedIn', () => false)
</script>
<!-- components/AppFooter.vue -->
<template>
<footer class="app-footer">
<p>© 2026 MegaShop. All rights reserved.</p>
<nav>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink to="/contact">Contact</NuxtLink>
<NuxtLink to="/privacy">Privacy Policy</NuxtLink>
</nav>
</footer>
</template>
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main>
<slot />
</main>
<AppFooter />
</div>
</template>
❓ FAQ
components/ directory, whether the filename starts with a capital letter, and whether the path has been excluded from the components configuration in nuxt.config.ts. Try restarting the dev server.Lazy prefix cause the component to render later?Lazy only delays the loading of the component's JavaScript code; once the component renders, it works normally. It performs code splitting, not deferred rendering.definePageMeta. If you need different layouts for different sections, achieve this by combining components.definePageMeta have to be at the top level of the script setup?definePageMeta is a compiler macro and must be called directly at the top level of <script setup>; it cannot be placed inside conditional statements or functions.components/ affect performance?📖 Summary
- Nuxt 3 Automatic Component Import: Generates component names based on directory paths, eliminating the need for manual imports
- The Layouts system uses
definePageMetato switch page layouts, andslotholds the page content - The "Lazy" prefix enables lazy loading of components, reducing the size of JavaScript above the fold
- Props/Emits: Implementing Type Safety Using TypeScript Generics
- MegaShop uses two layouts—"default" and "sidebar"—for both the front end and the admin panel
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Create the AppHeader and AppFooter components and use them in the default layout.
- Advanced Exercise (Difficulty: ⭐⭐): Create a sidebar layout and toggle between the two layouts on the admin page using
definePageMeta. Compare the results of the two layouts. - Challenge (Difficulty: ⭐⭐⭐): Use the Lazy prefix and v-if to implement a product details popup component that loads only when clicked, and use Suspense to display the loading state.
---|



