Slots, dynamic and async components
Named and scoped slots, switching components at runtime with keep-alive, and code-splitting with defineAsyncComponent and Suspense.
Named and scoped slots
<!-- DataTable.vue — template block -->
<table>
<thead>
<tr><th v-for="column in columns" :key="column">{{ column }}</th></tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<slot name="row" :row="row" :index="row.index">
<td>{{ row.name }}</td> <!-- fallback when the parent passes nothing -->
</slot>
</tr>
</tbody>
<tfoot><slot name="footer" /></tfoot>
</table>
<!-- parent: the table owns the layout, the parent owns the cells -->
<DataTable :rows="users" :columns="['Name', 'Email']">
<template #row="{ row }">
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
</template>
<template #footer>Total: {{ users.length }}</template>
</DataTable>- A slot passes markup, a prop passes data. The child keeps control of structure while the caller decides what appears inside it.
- Scoped slots reverse the flow: the child exposes its own row through slot props and the parent decides how to render it. That is what makes a generic table reusable.
#row="{ row }"is shorthand forv-slot:row="{ row }"; destructuring the slot props is the normal style.- Content inside the slot tags is only a fallback. It renders when the parent supplies nothing, which keeps an optional slot from becoming a crash.
- Slot content is compiled in the parent's scope, so it can read the parent's state but cannot reach the child's internals.
Dynamic components and keep-alive
import { shallowRef } from 'vue'
import ProfileTab from './ProfileTab.vue'
import BillingTab from './BillingTab.vue'
const tabs = { profile: ProfileTab, billing: BillingTab }
const currentTab = shallowRef(ProfileTab) // component definitions are not reactive data<component :is="tabs[current]" :user="user" />
<!-- cache the heavy tab so scroll position and local state survive switching -->
<KeepAlive :max="3" :include="['ProfileTab', 'BillingTab']">
<component :is="tabs[current]" :user="user" />
</KeepAlive><component :is>renders whatever component the value points at, which is the idiomatic way to build tabs, wizards and plugin surfaces.- Store the component definition in a
shallowRefor wrap it withmarkRaw. Making a component object deeply reactive is a real performance bug, not a style preference. KeepAlivecaches instances instead of destroying them, so scroll position, focus and local state survive. Cached components fireonActivatedandonDeactivatedon each switch.:includeand:excludematch on the component's name, and:maxcaps the cache. Without a limit, a long session keeps growing.- Passing a string to
:isonly resolves globally registered components, which is one more reason to import locally and pass the object.
💡
KeepAlive trades memory for state: every cached instance retains its data, its watchers and its DOM. Wrap only the parts a user actually switches between, and cap the cache with :max.Async components and Suspense
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: Spinner,
errorComponent: LoadFailed,
delay: 200, // avoids a spinner flash when the chunk is already cached
timeout: 10000
})<Suspense>
<template #default><AsyncDashboard /></template>
<template #fallback><Skeleton /></template>
</Suspense>defineAsyncComponentputs the component in its own chunk, fetched the first time it renders. The router's() => import(...)does the same thing for a whole route.- Use it for below-the-fold panels, modals and heavy editors. An async component that is always in the first paint only adds a round trip.
delayhides the spinner for a chunk already in the browser cache;timeoutturns a hanging network into a handled error state.- A
Suspenseboundary waits for async setup in its default slot and shows the fallback meanwhile. One slow child holds the whole boundary back, so keep boundaries small. - Suspense is still an experimental API: its behaviour can change between minor versions, so avoid making a critical flow depend on it without a non-Suspense path.
FAQ
Slot or prop?
If the caller needs to pass data, use a prop. If the caller needs to pass elements, multiple fragments or differently styled markup, use a slot. Scoped slots are for when the child needs to hand back its own data for the caller to render.
Should I use Suspense in production?
Only with care. It is documented as experimental, and the ordinary alternatives —
v-if on a loading flag, or an async component's loadingComponent — cover most cases without depending on an unstable API.Related
Composables and reusable logic Performance, rendering and SSR basics
Last refreshed 2026-09-18.