# Dynamic and Built-In Components

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Advanced Components
Source: https://www.devtutorials.ai/learn/vue/advanced-components/dynamic-and-built-in-components

> Switching components with <component :is>, async components with defineAsyncComponent() and their loading, error, delay and timeout options, code splitting, <KeepAlive> with include, exclude, max and activation hooks, <Teleport> for modals and overlays with focus handling, and the experimental <Suspense>.

## Components That Change

- Sometimes **which** component to show is decided while the app runs.
- Tabs, wizard steps and widgets on a dashboard all work like this.
- Vue has one tag for it, `<component :is>`, and four built-in helpers:
  - `defineAsyncComponent` loads a component only when needed
  - `<KeepAlive>` remembers a component's state when it is hidden
  - `<Teleport>` shows content somewhere else on the page
  - `<Suspense>` waits for async components to be ready

**Simple lesson:** `:is` picks a component at run time. Built-in components control how it loads, lives and where it appears.

### A Real-World Example

- Meera's dish page has three tabs: Details, Reviews and Nutrition.
- Each tab is its own component.
- One `<component :is>` shows the chosen one.

const tabs = { Details: DetailsTab, Reviews: ReviewsTab, Nutrition: NutritionTab };
const current = shallowRef(DetailsTab);
</script>

<template>
    <button v-for="(comp, name) in tabs" :key="name" :class="{ on: current === comp }" @click="current = comp">
      {{ name }}
    </button>
    <component :is="current" dish="Masala Dosa" />
</template>

<style>
.on { background: #2f9e6e; color: white; }
</style>
`,
    "DetailsTab.vue": `<script setup lang="ts">
defineProps({ dish: String });
</script>

<template>
    <h3>{{ dish }}</h3>
    <p>Crispy rice crepe with potato filling.</p>
</template>
`,
    "ReviewsTab.vue": `<template>
    <h3>Reviews</h3>
    <p>⭐⭐⭐⭐⭐ "Best in town!"</p>
</template>
`,
    "NutritionTab.vue": `<template>
    <h3>Nutrition</h3>
    <p>About 350 kcal per plate.</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** each button changed `current`. `<component :is>` showed that component.
- `dish` was passed as a prop, like on any component.

### What You Will Learn

1. How `<component :is>` works.
2. Why to store components in `shallowRef`.
3. How to load a component later with `defineAsyncComponent`.
4. Loading, error, delay and timeout options.
5. How code splitting makes the first load faster.
6. How `<KeepAlive>` keeps state.
7. The `onActivated` and `onDeactivated` hooks.
8. How `<Teleport>` moves a modal to the end of the page.
9. What `<Suspense>` does, and why it is still experimental.

## component :is

- `:is` takes a **component object** or the **name** of a registered component.
- It can also take an HTML tag name, such as `"button"` or `"a"`.
- Props, events and slots work as usual.
- Switching removes the old component and its state. `<KeepAlive>` changes that, below.

Store component objects in `shallowRef` or a plain object, not `ref`. A deep `ref` would make the whole component definition reactive, which is wasted work. Vue warns about it.

**Simple lesson:** `<component :is="X" />` renders whatever component `X` holds.

## Async Components

- `defineAsyncComponent(() => import("./Big.vue"))` makes a component that loads its code the first time it is shown.
- Until then, its code is not part of the first download.
- **Code splitting** = the build puts that file in its own small chunk.
- Options make it friendly:
  - `loadingComponent`: shown while loading
  - `delay`: wait this long before showing it (default 200 ms), to avoid a flash
  - `errorComponent`: shown if loading fails
  - `timeout`: give up after this long

function slow<T>(load: () => Promise<T>, ms: number): () => Promise<T> {
    return () => new Promise((resolve) => setTimeout(() => resolve(load()), ms));
}

const MenuChart = defineAsyncComponent({
    loader: slow(() => import("./MenuChart.vue"), 1200),
    loadingComponent: LoadingBox,
    delay: 200,
    errorComponent: ErrorBox,
    timeout: 5000,
});

const BrokenWidget = defineAsyncComponent({
    loader: () => Promise.reject(new Error("network down")),
    errorComponent: ErrorBox,
});

const show = ref<boolean>(false);
</script>

<template>
    <button @click="show = true">Show sales chart</button>
    <template v-if="show">
    </template>
</template>
`,
    "MenuChart.vue": `<script setup lang="ts">
console.log("MenuChart code ran (loaded on demand)");
</script>

<template>
    <p>📊 Dosa 42 · Idli 30 · Vada 18</p>
</template>
`,
    "LoadingBox.vue": `<template>
    <p>Loading the chart…</p>
</template>
`,
    "ErrorBox.vue": `<template>
    <p style="color: crimson">This part could not load. Please try again.</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "Loading the chart…" showed, then the chart.
- The broken widget showed its error component instead of a blank space.

In a real build, `import("./MenuChart.vue")` downloads a separate JavaScript file the first time. In this lab, every file is already in the preview, so the box adds a fake delay to show the loading state. Routes can be split the same way, as in **Navigation Guards and Advanced Routing**.

**Simple lesson:** `defineAsyncComponent` loads code only when needed. Give it loading and error parts.

## KeepAlive

- Normally, switching away from a component destroys it and its state.
- Wrap the dynamic component in `<KeepAlive>` to keep it in memory instead.
- `include` and `exclude` choose which components to keep, by name. A `<script setup>` component is named after its file (`Reviews.vue` → `Reviews`), or set one with `defineOptions({ name })`.
- `max` limits how many are kept. The least recently used is dropped first.
- Kept components get `onActivated` and `onDeactivated` hooks instead of mount and unmount.

const current = shallowRef(OrderNote);
</script>

<template>
    <button @click="current = OrderNote">Note</button>
    <button @click="current = TableMap">Tables</button>
      <component :is="current" />
</template>
`,
    "OrderNote.vue": `<script setup lang="ts">

onActivated(() => console.log("OrderNote shown again (activated)"));
onDeactivated(() => console.log("OrderNote hidden but kept (deactivated)"));
</script>

<template>
    <textarea placeholder="Type a note, switch tabs, come back"></textarea>
</template>
`,
    "TableMap.vue": `<template>
    <p>Tables 1 to 8: 3 free</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** your note was still there after switching back.
- The Console shows `deactivated` and `activated` instead of unmount and mount.

`include` and `exclude` match the component's **name**. A `<script setup>` component is named after its file, such as `OrderNote`. Use `defineOptions({ name })` if you need a different name.

**Simple lesson:** `<KeepAlive>` hides components instead of destroying them.

## Teleport

- A modal is written inside a component, but it should appear on top of the **whole page**.
- Deep inside other elements, CSS like `overflow: hidden` or `z-index` can trap it.
- `<Teleport to="body">` renders its content at the end of `<body>`, but keeps it part of the component.
- State, props and events still work normally.
- `:disabled="true"` keeps it in place, for example on small screens.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-teleport" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-teleport">Animation: a modal written inside OrderCard is moved by Teleport to the end of the body element, outside the card, while its logic stays in the component</title>
  <rect x="20" y="20" width="280" height="160" rx="12" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)", strokeWidth: 1.5 }} />
  <text x="40" y="44" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>{"<div id=\"app\">"}</text>
  <rect x="45" y="60" width="230" height="100" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} />
  <text x="60" y="82" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>OrderCard (overflow: hidden)</text>
  <rect x="340" y="20" width="280" height="160" rx="12" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)", strokeWidth: 1.5 }} />
  <text x="360" y="44" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>end of {"<body>"}</text>
  <g>
    <animateTransform attributeName="transform" type="translate" values="0 0;0 0;300 0;300 0;0 0" keyTimes="0;0.2;0.5;0.95;1" dur="6s" repeatCount="indefinite" />
    <rect x="75" y="95" width="170" height="50" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 2 }} />
    <text x="160" y="125" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Confirm order? modal</text>
  </g>
  <text x="320" y="196" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>Written in OrderCard. Shown at the end of body. Logic stays in OrderCard.</text>
</svg>
<figcaption>Teleport moves where the HTML appears, not where the code lives.</figcaption>
</figure>

</script>

<template>
    <div class="frame">
    </div>
</template>

<style>
.frame { overflow: hidden; height: 90px; border: 1px dashed #999; }
</style>
`,
    "OrderCard.vue": `<script setup lang="ts">

const open = ref<boolean>(false);
const confirmBtn = useTemplateRef<HTMLButtonElement>("confirmBtn");
const opener = useTemplateRef<HTMLButtonElement>("opener");

async function show(): Promise<void> {
    open.value = true;
    await nextTick();
    confirmBtn.value?.focus();
}

async function close(answer: string): Promise<void> {
    console.log("Answer:", answer, "| modal parent was:", document.querySelector(".modal")?.parentElement?.tagName);
    open.value = false;
    await nextTick();
    opener.value?.focus();
}
</script>

<template>
    <p>Order #501: 2 × Dosa</p>
    <button ref="opener" @click="show">Place order</button>
      <div v-if="open" class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" @keydown.esc="close('cancel')">
        <h3 id="modal-title">Confirm order?</h3>
        <button ref="confirmBtn" @click="close('yes')">Yes, place it</button>
        <button @click="close('cancel')">Cancel</button>
      </div>
</template>

<style>
.modal { position: fixed; inset: 30px 20px auto; background: white; border: 2px solid #2f9e6e; border-radius: 12px; padding: 12px; box-shadow: 0 8px 30px #0004; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the modal escaped the card's `overflow: hidden` box.
- Focus moved into the modal, and back to the button after closing. Esc also closes it.

A good modal: has `role="dialog"` and `aria-modal="true"`, moves focus inside when it opens, closes on Esc, and returns focus to the button that opened it.

**Simple lesson:** `<Teleport to="body">` puts modals and overlays at the end of the page.

## Suspense (Experimental)

- An **async component** here means a `<script setup>` that uses `await` at the top level.
- `<Suspense>` waits until every async child below it is ready.
- Until then, it shows its `#fallback` slot.
- Events: `pending`, `resolve` and `fallback`.
- It is still **experimental**: its API may change. Many apps show their own loading state instead.

</script>

<template>
      <template #fallback>
        <p>Asking the kitchen…</p>
      </template>
</template>
`,
    "DishOfTheDay.vue": `<script setup lang="ts">
interface Dish {
    name: string;
    price: number;
}

const res = await fetch("https://api.meeraskitchen.dev/dishes/2?delay=1000");
const dish = (await res.json()) as Dish;
</script>

<template>
    <h3>Today: {{ dish.name }}, ₹{{ dish.price }}</h3>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `<Suspense>` shows a fallback until async children are ready. Use it with care.

## TL;DR

- **`<component :is>`** = renders the component (or tag) held in a variable.
- **`shallowRef`** = the right way to store a component object.
- **`defineAsyncComponent`** = loads a component's code on first use.
- **Async options** = `loadingComponent`, `delay`, `errorComponent`, `timeout`.
- **Code splitting** = lazy parts become separate, smaller files.
- **`<KeepAlive>`** = keeps hidden components and their state. `include`, `exclude`, `max`.
- **`onActivated` / `onDeactivated`** = hooks for kept components.
- **`<Teleport to="body">`** = renders content elsewhere, keeps the logic here.
- **`<Suspense>`** = experimental. Shows `#fallback` until async children are ready.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Component in a deep `ref` | Any ref works | Use `shallowRef` | A warning and wasted work |
| No loading or error component | Lazy parts load instantly | Set `loadingComponent` and `errorComponent` | Blank space, or nothing on failure |
| Expecting state to survive a switch | Vue keeps it | Wrap in `<KeepAlive>` | Typed text is lost |
| `include` with the wrong name | It matches the tag | It matches the component name | Nothing is kept |
| Modal inside `overflow: hidden` | z-index fixes it | `<Teleport to="body">` | The modal is cut off or hidden |
| Modal without focus handling | Clicking is enough | Focus in, Esc to close, focus back | Keyboard users get lost |
| Suspense everywhere | It is a stable core feature | Use it sparingly, it is experimental | Code may break on upgrades |

</div>

## Check Yourself

## Exercises

## What to Remember

- `<component :is>` switches components. Store them in `shallowRef`.
- `defineAsyncComponent` loads code on demand. Add loading and error parts.
- `<KeepAlive>` keeps state. Kept components get `onActivated` and `onDeactivated`.
- `<Teleport to="body">` frees modals from parent CSS. Handle focus and Esc.
- `<Suspense>` is experimental. Prefer your own loading states for now.

Next: **Transitions and Animations**
