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 for v-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 shallowRef or wrap it with markRaw. Making a component object deeply reactive is a real performance bug, not a style preference.
  • KeepAlive caches instances instead of destroying them, so scroll position, focus and local state survive. Cached components fire onActivated and onDeactivated on each switch.
  • :include and :exclude match on the component's name, and :max caps the cache. Without a limit, a long session keeps growing.
  • Passing a string to :is only 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>
  • defineAsyncComponent puts 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.
  • delay hides the spinner for a chunk already in the browser cache; timeout turns a hanging network into a handled error state.
  • A Suspense boundary 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.

Composables and reusable logic Performance, rendering and SSR basics

Last refreshed 2026-09-18.