# Lifecycle and Template Refs

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Components
Source: https://www.devtutorials.ai/learn/vue/components/lifecycle-and-template-refs

> The life of a component from creation to mount, update and unmount, the Composition API lifecycle hooks with an Options API mapping, template refs with useTemplateRef(), refs inside v-for, component refs and defineExpose(), and common uses: DOM access, listeners, timers, requests and cleanup.

## The Life of a Component

- A component is born, shown, updated and finally removed.
- These stages are its **lifecycle**.
- **Lifecycle hook** = a function Vue calls at one stage, such as "just shown".
- You use hooks to start and stop work that lives outside Vue: timers, listeners and requests.

**Simple lesson:** hooks let you run code when a component appears, changes or leaves.

### A Real-World Example

- Meera's kitchen screen shows how long an order has been waiting.
- The timer must start when the order card appears.
- When the order is served, the card goes away. The timer must stop too.

const waiting = ref<boolean>(true);
</script>

<template>
    <button @click="waiting = !waiting">{{ waiting ? "Serve the order" : "New order" }}</button>
</template>
`,
    "OrderTimer.vue": `<script setup lang="ts">

defineProps({ table: String });

const seconds = ref<number>(0);
let timer: number | undefined;

onMounted(() => {
    console.log("Timer started");
    timer = window.setInterval(() => seconds.value++, 1000);
});

onUnmounted(() => {
    window.clearInterval(timer);
    console.log("Timer stopped");
});
</script>

<template>
    <p>Table {{ table }} waiting: {{ seconds }} s</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** `onMounted` started the timer once the card was on the page.
- "Serve the order" removed the card. `onUnmounted` stopped the timer.

### What You Will Learn

1. The four stages of a component's life.
2. The Composition API hooks, and their Options API names.
3. When each hook runs.
4. How to reach a DOM element with a template ref.
5. How to collect refs inside `v-for`.
6. How to call a child component's functions.
7. How `defineExpose()` decides what a parent may use.
8. Common jobs for hooks, and how to clean up.

## Lifecycle Stages

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-vue-life" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-life">Animation: a dot moves along the lifecycle. Setup, before mount, mounted, then a loop of before update and updated, then before unmount and unmounted</title>
  <path d="M30 70 H610" style={{ stroke: "var(--ngc-slate)", strokeWidth: 3 }} />
  <path d="M330 70 C330 20 450 20 450 70" style={{ fill: "none", stroke: "var(--ngc-purple)", strokeWidth: 2, strokeDasharray: "5 4" }} />
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <circle cx="40" cy="70" r="7" style={{ fill: "var(--ngc-blue)" }} /><text x="40" y="100" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>setup</text>
    <circle cx="140" cy="70" r="7" style={{ fill: "var(--ngc-blue)" }} /><text x="140" y="100" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>onBeforeMount</text>
    <circle cx="240" cy="70" r="7" style={{ fill: "var(--ngc-green)" }} /><text x="240" y="100" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>onMounted</text>
    <circle cx="330" cy="70" r="7" style={{ fill: "var(--ngc-purple)" }} /><text x="330" y="100" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>onBeforeUpdate</text>
    <circle cx="450" cy="70" r="7" style={{ fill: "var(--ngc-purple)" }} /><text x="450" y="118" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>onUpdated</text>
    <circle cx="530" cy="70" r="7" style={{ fill: "var(--ngc-orange)" }} /><text x="530" y="100" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>onBeforeUnmount</text>
    <circle cx="610" cy="70" r="7" style={{ fill: "var(--ngc-red)" }} /><text x="600" y="118" textAnchor="middle" style={{ fill: "var(--ngc-red)" }}>onUnmounted</text>
  </g>
  <text x="390" y="30" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>state changes: repeat</text>
  <circle r="9" style={{ fill: "var(--ngc-yellow)", stroke: "var(--ngc-slate)" }}><animateMotion dur="8s" repeatCount="indefinite" keyPoints="0;0.33;0.5;0.68;1" keyTimes="0;0.25;0.45;0.65;1" path="M40 70 H330 C330 20 450 20 450 70 H610" /></circle>
  <text x="320" y="155" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>Created → shown → updated (many times) → removed</text>
</svg>
<figcaption>A component is set up, mounted, updated many times, then unmounted.</figcaption>
</figure>

- **Setup** = `<script setup>` runs. State is created. Nothing is on the page yet.
- **Mount** = Vue builds the elements and puts them on the page.
- **Update** = state changed, so Vue updates the page.
- **Unmount** = the component is removed, for example by `v-if`.

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

<template>
    <button @click="show = !show">{{ show ? "Remove" : "Add" }} the component</button>
</template>
`,
    "Chatty.vue": `<script setup lang="ts">

const likes = ref<number>(0);
console.log("1. setup: state ready");

onBeforeMount(() => console.log("2. onBeforeMount: not on the page yet"));
onMounted(() => console.log("3. onMounted: on the page now"));
onBeforeUpdate(() => console.log("4. onBeforeUpdate: likes =", likes.value));
onUpdated(() => console.log("5. onUpdated: page shows new likes"));
onBeforeUnmount(() => console.log("6. onBeforeUnmount: still on the page"));
onUnmounted(() => console.log("7. onUnmounted: gone"));
</script>

<template>
    <button @click="likes++">❤ {{ likes }}</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** the Console shows steps 1 to 3 at the start.
- Each like shows 4 and 5. "Remove" shows 6 and 7.

### Hooks in the Options API

- Older code uses the Options API names. You will learn that style in **Options API**.

<div className="tscroll">

| Composition API | Options API | Runs |
|---|---|---|
| `<script setup>` itself | `beforeCreate`, `created` | Before anything is on the page |
| `onBeforeMount` | `beforeMount` | Just before the first render |
| `onMounted` | `mounted` | After the elements are on the page |
| `onBeforeUpdate` | `beforeUpdate` | Before the page is updated |
| `onUpdated` | `updated` | After the page is updated |
| `onBeforeUnmount` | `beforeUnmount` | Before removal |
| `onUnmounted` | `unmounted` | After removal |

</div>

Call hooks at the top level of `<script setup>`, never later inside a `setTimeout` or after an `await`. Vue must know which component a hook belongs to when it is registered.

**Simple lesson:** `onMounted` to start, `onUnmounted` to clean up.

## Template Refs

- Sometimes you need the real DOM element: to focus a box, measure a size or call a browser API.
- Give the element a `ref="name"` attribute.
- In the script, `useTemplateRef("name")` (Vue 3.5) gives you a ref to it.
- It is `null` until the component is mounted. Use it in `onMounted` or later.
- Code written before Vue 3.5 uses `const box = ref(null)` with the **same name** as `ref="box"`. It does the same job.

const search = useTemplateRef<HTMLInputElement>("searchBox");

onMounted(() => {
    search.value?.focus();
    console.log("Box width:", search.value?.offsetWidth, "px");
});

function clear(): void {
    if (search.value) search.value.value = "";
    search.value?.focus();
}
</script>

<template>
    <input ref="searchBox" placeholder="Search dishes" />
    <button @click="clear">Clear</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

Before Vue 3.5, code used a plain ref with the same name: `const searchBox = ref(null)` with `ref="searchBox"`. You will still see this in older projects. It works the same way.

### Refs Inside v-for

- A `ref` inside `v-for` gives an **array** of elements.
- The order of the array may not match the order of the list. Use it for counting or looping, not by position.

const dishes: string[] = ["Dosa", "Idli", "Vada", "Pongal"];
const items = useTemplateRef<HTMLLIElement[]>("rows");

function highlight(): void {
    items.value?.forEach((li) => (li.style.background = "#fff3c4"));
    console.log("Rows found:", items.value?.length);
}
</script>

<template>
    <ul>
      <li v-for="d in dishes" :key="d" ref="rows">{{ d }}</li>
    </ul>
    <button @click="highlight">Highlight all</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

## Component Refs and defineExpose()

- A `ref` on a **component** gives you that component's instance.
- With `<script setup>`, a component is **closed** by default. The parent sees nothing inside.
- The child chooses what to share with `defineExpose({ ... })`.
- Use this rarely. Props and events are usually the better way.

const form = useTemplateRef<InstanceType<typeof OrderForm>>("orderForm");
</script>

<template>
    <button @click="form?.reset()">Reset from the parent</button>
    <button @click="console.log('Plates in child:', form?.plates)">Read plates</button>
</template>
`,
    "OrderForm.vue": `<script setup lang="ts">

const plates = ref<number>(1);
const secret = "only for this component";

function reset(): void {
    plates.value = 1;
}

defineExpose({ reset, plates });
</script>

<template>
    <p>Plates: {{ plates }} <button @click="plates++">+</button></p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** the parent called `reset()` and read `plates`.
- `secret` was not exposed, so the parent cannot see it.

**Simple lesson:** `ref` + `useTemplateRef()` reach elements and components. `defineExpose()` decides what a child shares.

## Common Jobs for Hooks

<div className="tscroll">

| Job | Start in | Clean up in |
|---|---|---|
| Focus or measure an element | `onMounted` | Nothing |
| Listen to `window` or `document` | `onMounted` | `onUnmounted` (remove the listener) |
| Timers: `setInterval`, `setTimeout` | `onMounted` | `onUnmounted` (clear it) |
| Load data from a server | `onMounted` or `<script setup>` | `onUnmounted` (abort the request) |
| A third-party chart or map | `onMounted` | `onUnmounted` (destroy it) |

</div>

const open = ref<boolean>(true);
</script>

<template>
    <button @click="open = !open">{{ open ? "Close" : "Open" }} the panel</button>
</template>
`,
    "KitchenPanel.vue": `<script setup lang="ts">

const width = ref<number>(window.innerWidth);
const dishes = ref<number>(0);
const controller = new AbortController();

function onResize(): void {
    width.value = window.innerWidth;
}

onMounted(async () => {
    window.addEventListener("resize", onResize);
    try {
      const res = await fetch("https://api.meeraskitchen.dev/dishes", { signal: controller.signal });
      dishes.value = ((await res.json()) as unknown[]).length;
    } catch {
      console.log("Request cancelled");
    }
});

onUnmounted(() => {
    window.removeEventListener("resize", onResize);
    controller.abort();
});
</script>

<template>
    <p>Screen width: {{ width }} px. Dishes on the menu: {{ dishes }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

Close the panel quickly, before the dishes load. The Console shows "Request cancelled", because `onUnmounted` aborted it.

**Simple lesson:** whatever you start in a hook, stop in `onUnmounted`.

## TL;DR

- **Lifecycle** = setup → mount → update (many times) → unmount.
- **`onMounted`** = the elements are on the page. Start DOM work, timers, listeners.
- **`onUpdated`** = the page shows the new state.
- **`onUnmounted`** = clean up timers, listeners and requests.
- **Options names** = `mounted`, `updated`, `unmounted` ...
- **Template ref** = `ref="name"` + `useTemplateRef("name")`.
- **Refs in `v-for`** = an array of elements.
- **Component ref** = the child's instance. Closed by default.
- **`defineExpose()`** = what a child shares with a parent's ref.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Using a template ref in setup | The element exists at once | Use it in `onMounted` or later | It is `null` |
| No cleanup for timers | Vue stops them | `clearInterval` in `onUnmounted` | Timers keep running after the component is gone |
| Hook after `await` | Hooks work anywhere | Register hooks at the top level | A warning, and the hook never runs |
| Changing state in `onUpdated` | It is a good place to fix values | Use `computed` or a watcher | An endless update loop |
| Reading a child's state without `defineExpose` | Parents see everything | Expose what is needed | The value is `undefined` |
| Refs in `v-for` by position | The order matches the list | Use them as an unordered set | The wrong element is changed |

</div>

## Check Yourself

## Exercises

## What to Remember

- Components go through setup, mount, update and unmount.
- Start DOM work in `onMounted`. Clean everything up in `onUnmounted`.
- Register hooks at the top level of `<script setup>`.
- `ref="name"` + `useTemplateRef("name")` reaches elements after mount.
- Component refs see only what `defineExpose()` shares.

Next: **Options API**
