Table of Contents
- Component Composition Patterns
- Slots: Beyond the Basics
- Provide/Inject for Deep Component Trees
- The Composition API for Reusable Logic
- Scoped Styles and Deep Selectors
- Dynamic and Async Components
- Dynamic Components with
<component :is="..." /> - Async Components and Code Splitting
- Dynamic Components with
- Advanced Component Communication
- v-model Arguments and Custom Modifiers
- Event Bus vs. State Management (Vuex/Pinia)
- Lifecycle Hooks: Beyond the Basics
- Error Handling with
errorCaptured - Render Tracking with
renderTracked/renderTriggered
- Error Handling with
- Testing Advanced Components
- Testing Slots and Scoped Slots
- Mocking Async Components
- Performance Optimization for Components
- Memoization with
v-memoandcomputed - Avoiding Unnecessary Re-renders
- Memoization with
- Conclusion
- References
1. Component Composition Patterns
Component composition is the art of combining smaller components into larger, more complex ones while keeping logic reusable and decoupled. Vue offers several powerful patterns to achieve this.
Slots: Beyond the Basics
Slots allow parent components to inject content into child components, enabling flexible and customizable UIs. While basic slots are straightforward, advanced use cases like named slots and scoped slots unlock greater flexibility.
Named Slots
Use named slots to define multiple insertion points in a child component. This is useful for components with distinct sections (e.g., headers, footers).
ChildComponent.vue
<template>
<div class="card">
<!-- Named slot for header -->
<slot name="header"></slot>
<!-- Default slot for main content -->
<slot></slot>
<!-- Named slot for footer -->
<slot name="footer"></slot>
</div>
</template>
ParentComponent.vue
<template>
<ChildComponent>
<template #header> <!-- Shorthand for v-slot:header -->
<h2>Card Title</h2>
</template>
<p>This is the main content of the card.</p> <!-- Default slot -->
<template #footer>
<button>Read More</button>
</template>
</ChildComponent>
</template>
Scoped Slots
Scoped slots allow the child component to pass data back to the parent, enabling the parent to customize content using child data. For example, a list component might let the parent define how each list item is rendered.
ListComponent.vue
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- Pass item data to the parent via scoped slot -->
<slot :item="item"></slot>
</li>
</ul>
</template>
<script>
export default {
props: { items: { type: Array, required: true } }
};
</script>
ParentComponent.vue
<template>
<ListComponent :items="users">
<!-- Use scoped slot to access child data -->
<template #default="{ item }"> <!-- Destructure the slot props -->
<div>Name: {{ item.name }}, Age: {{ item.age }}</div>
</template>
</ListComponent>
</template>
<script>
export default {
data() {
return {
users: [{ id: 1, name: "Alice", age: 30 }, { id: 2, name: "Bob", age: 25 }]
};
}
};
</script>
Provide/Inject for Deep Component Trees
Props work well for parent-child communication, but passing props through multiple levels (prop drilling) becomes cumbersome. Provide/Inject lets a parent component “provide” data, which any descendant component can “inject”—no matter how deep the hierarchy.
ParentComponent.vue
<script>
export default {
provide() {
return {
theme: "dark", // Provide data to descendants
updateTheme: (newTheme) => this.theme = newTheme // Provide a method
};
},
data() { return { theme: "dark" }; }
};
</script>
DeepChildComponent.vue
<script>
export default {
inject: ["theme", "updateTheme"], // Inject data from ancestor
mounted() {
console.log("Current theme:", this.theme); // Logs "dark"
this.updateTheme("light"); // Call provided method to update
}
};
</script>
Note: Use provide/inject sparingly for app-wide configurations (e.g., themes, authentication). For dynamic state, prefer Pinia/Vuex.
The Composition API for Reusable Logic
The Composition API (via <script setup>) enables extracting and reusing component logic across multiple components using composables (functions that return reactive state and methods).
useCounter.js (Composable)
import { ref, computed } from "vue";
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const doubleCount = computed(() => count.value * 2);
const increment = () => count.value++;
return { count, doubleCount, increment };
}
CounterComponent.vue
<script setup>
import { useCounter } from "./useCounter";
const { count, doubleCount, increment } = useCounter(10); // Reuse logic
</script>
<template>
<div>
Count: {{ count }} <br>
Double: {{ doubleCount }} <br>
<button @click="increment">Increment</button>
</div>
</template>
2. Scoped Styles and Deep Selectors
Vue’s scoped styles (<style scoped>) prevent CSS leakage by adding unique attributes to elements. However, you may need to target child components or third-party libraries. Use deep selectors to bypass scoping.
Scoped Styles Basics
<style scoped>
/* Only affects elements in this component */
.card { color: red; }
</style>
Deep Selectors
To style child components or elements inside slots, use :deep() (Vue 3) or ::v-deep (Vue 2).
Targeting a Child Component’s Class
<style scoped>
/* Styles .child-class inside ChildComponent */
:deep(.child-class) {
margin: 1rem;
}
</style>
Targeting Slot Content
Use ::v-slotted() to style content injected via slots:
<style scoped>
::v-slotted(p) { /* Styles <p> tags in slots */
font-size: 1.2rem;
}
</style>
3. Dynamic and Async Components
Dynamic components let you render different components based on a reactive value, while async components load component code on demand (improving initial load time).
Dynamic Components with <component :is="..." />
Use the <component> element with the :is directive to render components dynamically.
Example: Tabbed Interface
<template>
<div>
<button @click="activeTab = 'Home'">Home</button>
<button @click="activeTab = 'Profile'">Profile</button>
<!-- Render component based on activeTab -->
<component :is="activeTabComponent"></component>
</div>
</template>
<script setup>
import Home from "./Home.vue";
import Profile from "./Profile.vue";
import { ref } from "vue";
const activeTab = ref("Home");
const activeTabComponent = computed(() => {
return activeTab.value === "Home" ? Home : Profile;
});
</script>
Async Components
Async components load their code only when needed (code splitting), reducing initial bundle size. Use defineAsyncComponent for this.
Basic Async Component
<script setup>
import { defineAsyncComponent } from "vue";
// Load HeavyComponent only when rendered
const HeavyComponent = defineAsyncComponent(() =>
import("./HeavyComponent.vue")
);
</script>
<template>
<HeavyComponent v-if="showHeavyComponent" />
<button @click="showHeavyComponent = true">Load Component</button>
</template>
Advanced: Loading/Error States
Handle loading and error states for async components:
<script setup>
const HeavyComponent = defineAsyncComponent({
loader: () => import("./HeavyComponent.vue"),
loadingComponent: LoadingSpinner, // Shown while loading
errorComponent: ErrorMessage, // Shown if load fails
delay: 200, // Delay before showing loading (avoids flicker)
timeout: 5000 // Fail after 5 seconds
});
</script>
4. Advanced Component Communication
Beyond props and events, Vue offers patterns for more nuanced component interactions.
v-model Arguments
Customize v-model to support multiple two-way bindings in a single component using v-model:argument.
CustomInput.vue
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
<input
:value="searchQuery"
@input="$emit('update:searchQuery', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue', 'searchQuery']); // Accept props
defineEmits(['update:modelValue', 'update:searchQuery']); // Emit updates
</script>
Parent.vue
<template>
<CustomInput
v-model="username"
v-model:searchQuery="query"
/>
</template>
<script setup>
import { ref } from "vue";
const username = ref("");
const query = ref("");
</script>
State Management Integration
For apps with complex state, use Pinia (Vue 3’s official store) to centralize component communication. Components dispatch actions or read state from the store, avoiding prop drilling.
Example with Pinia
// stores/cart.js
import { defineStore } from "pinia";
export const useCartStore = defineStore("cart", {
state: () => ({ items: [] }),
actions: { addItem(item) { this.items.push(item); } }
});
Component Using the Store
<script setup>
import { useCartStore } from "./stores/cart";
const cart = useCartStore();
cart.addItem({ id: 1, name: "Laptop" }); // Call store action
</script>
5. Lifecycle Hooks and Advanced Use Cases
Vue’s lifecycle hooks let you intercept component behavior at key stages. Beyond common hooks like onMounted, lesser-known hooks solve specific problems.
errorCaptured
Catch errors from child components and their descendants to prevent app crashes.
<script setup>
import { onErrorCaptured } from "vue";
onErrorCaptured((err, instance, info) => {
console.error("Error captured:", err, info);
// Return true to prevent the error from propagating further
return true;
});
</script>
renderTracked and renderTriggered
Debug reactivity by tracking when dependencies are tracked or re-renders are triggered.
<script setup>
import { onRenderTracked, onRenderTriggered } from "vue";
onRenderTracked((event) => {
console.log("Tracked dependency:", event); // Logs when a reactive value is tracked
});
onRenderTriggered((event) => {
console.log("Render triggered by:", event); // Logs why a re-render happened
});
</script>
6. Testing Advanced Components
Testing components with slots, async logic, or complex state requires specialized approaches. Use Vue Test Utils for this.
Testing Slots
Use slots option to pass content to slots in tests.
import { mount } from "@vue/test-utils";
import Card from "./Card.vue";
test("renders header slot content", () => {
const wrapper = mount(Card, {
slots: {
header: "<h2>Test Header</h2>" // Inject header slot content
}
});
expect(wrapper.find("h2").text()).toBe("Test Header");
});
Mocking Async Components
Mock async components to avoid network requests in tests.
import { defineAsyncComponent } from "vue";
// Mock the async component
jest.mock("./HeavyComponent.vue", () => ({
default: { template: "<div>Mocked Heavy Component</div>" }
}));
test("renders mocked async component", async () => {
const AsyncComponent = defineAsyncComponent(() =>
import("./HeavyComponent.vue")
);
const wrapper = mount(AsyncComponent);
await wrapper.vm.$nextTick(); // Wait for async resolution
expect(wrapper.text()).toContain("Mocked Heavy Component");
});
7. Performance Optimization for Components
Poorly optimized components can lead to slow apps. Use these techniques to keep components snappy.
Memoization with v-memo
v-memo skips re-rendering an element if its dependencies haven’t changed, similar to React’s memo.
<template>
<div v-memo="[items.length]"> <!-- Only re-render if items.length changes -->
<p v-for="item in items" :key="item.id">{{ item.name }}</p>
</div>
</template>
Keep Components Small
Split large components into smaller, focused ones. For example, a UserProfile component might split into ProfileHeader, ProfilePosts, and ProfileStats.
Virtual Scrolling for Large Lists
For lists with thousands of items, use virtual scrolling libraries like vue-virtual-scroller to render only visible items.
Conclusion
Mastering advanced Vue.js components unlocks the ability to build scalable, maintainable, and performant applications. By leveraging composition patterns, dynamic/async components, deep communication strategies, and optimization techniques, you can create UIs that are both flexible and efficient.
Remember, the best way to internalize these concepts is through practice. Experiment with slots, build custom composables, and optimize existing components to see firsthand how these techniques improve your workflow.