---
title: "Lifecycle hooks and template refs, with useTemplateRef() · vuejs.school"
description: "Learn when onMounted, onUpdated and onUnmounted fire, why DOM work waits for mount, and how useTemplateRef() reaches a real element or a child component."
canonical_url: "https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs"
meta:
  "og:description": "Learn when onMounted, onUpdated and onUnmounted fire, why DOM work waits for mount, and how useTemplateRef() reaches a real element or a child component."
  "og:image": "https://vuejs.school/_og/d/a_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCk%2Cdescription_~TGVhcm4gd2hlbiBvbk1vdW50ZWQsIG9uVXBkYXRlZCBhbmQgb25Vbm1vdW50ZWQgZmlyZSwgd2h5IERPTSB3b3JrIHdhaXRzIGZvciBtb3VudCwgYW5kIGhvdyB1c2VUZW1wbGF0ZVJlZigpIHJlYWNoZXMgYSByZWFsIGVsZW1lbnQgb3IgYSBjaGlsZCBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9saWZlY3ljbGUtaG9va3MtYW5kLXRlbXBsYXRlLXJlZnMi%2Cs_tF5SREA7_eDtFXOF.png"
  "og:image:alt": "Lifecycle hooks and template refs, with useTemplateRef() · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Lifecycle hooks and template refs, with useTemplateRef() · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCk%2Cdescription_~TGVhcm4gd2hlbiBvbk1vdW50ZWQsIG9uVXBkYXRlZCBhbmQgb25Vbm1vdW50ZWQgZmlyZSwgd2h5IERPTSB3b3JrIHdhaXRzIGZvciBtb3VudCwgYW5kIGhvdyB1c2VUZW1wbGF0ZVJlZigpIHJlYWNoZXMgYSByZWFsIGVsZW1lbnQgb3IgYSBjaGlsZCBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9saWZlY3ljbGUtaG9va3MtYW5kLXRlbXBsYXRlLXJlZnMi%2Cs_tF5SREA7_eDtFXOF.png"
  "twitter:image:alt": "Lifecycle hooks and template refs, with useTemplateRef() · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## When onMounted, onUpdated and onUnmounted run

A component does not exist all at once. Vue runs your `<script setup>` first, when there is no HTML yet. Then it renders the template and inserts the result into the page; that moment is called **mounting**. Every time reactive data changes, Vue re-renders the parts that depend on it; that is an **update**. When the component is removed, because a `v-if` turned false or the user navigated away, it is **unmounted**.

Lifecycle hooks are functions you call in `<script setup>` to run code at those moments. The three you will use most are `onMounted`, `onUpdated` and `onUnmounted`. Each takes a function:

*components/LifecycleLog.vue*

```
<script setup lang="ts">
import { onMounted, onUnmounted, onUpdated, ref } from 'vue'

const count = ref(0)

onMounted(() => console.log('mounted: the DOM exists now'))
onUpdated(() => console.log('updated: the DOM was re-rendered'))
onUnmounted(() => console.log('unmounted: the DOM is gone'))
</script>

<template>
  <button type="button" @click="count++">Clicked {{ count }} times</button>
</template>
```

Open the console and click. You see `mounted` once, `updated` after every click, and `unmounted` when the parent removes the component. There are also `onBeforeMount`, `onBeforeUpdate` and `onBeforeUnmount`, which fire just before each step; `onBeforeUnmount` is the one worth remembering, because the DOM is still there when it runs.

Register hooks while Vue is setting the component up: at the top level of `<script setup>`, or inside a function that `<script setup>` calls straight away. Vue attaches each hook to the component it is setting up at that moment. In a `setTimeout` callback, a click handler, or the part of an async function that runs after an `await`, there is no such component, so the hook never runs and Vue prints a warning in development.

## Why DOM work waits for onMounted

A **template ref** is how script code gets hold of an element from the template: give the element a `ref` attribute with a name and pass the same name to `useTemplateRef()`. It returns a ref whose `.value` holds the element. Say you want a search box focused as soon as it appears, so you call `focus()` straight after asking for the element:

*components/SearchBox.vue**Breaks***

```
<script setup lang="ts">
import { useTemplateRef } from 'vue'

const input = useTemplateRef<HTMLInputElement>('query')

input.value.focus()
</script>

<template>
  <input ref="query" type="search" placeholder="Search lessons">
</template>
```

In Chrome this throws `TypeError: Cannot read properties of null (reading 'focus')`. `<script setup>` runs before the template is rendered, so at that line the `<input>` does not exist yet and `input.value` is `null`. The element only exists after mount, so that is where the call goes:

*components/SearchBox.vue**Works***

```
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'

const input = useTemplateRef<HTMLInputElement>('query')

onMounted(() => {
  input.value?.focus()
})
</script>

<template>
  <input ref="query" type="search" placeholder="Search lessons">
</template>
```

The `?.` is optional chaining: it calls `focus()` only if `input.value` is not `null`. Without it, `vue-tsc` reports `'input.value' is possibly 'null'`, because the ref's type is `HTMLInputElement | null`. That type is accurate: the value is `null` before mount, and it becomes `null` again if a `v-if` removes the element later.

## Get elements and child components with useTemplateRef()

The string you pass to `useTemplateRef()` is the only link between the script and the `ref` attribute, so the variable can be called anything: in `SearchBox.vue` the attribute is `query` and the variable is `input`.

The generic argument, `useTemplateRef<HTMLInputElement>('query')`, tells TypeScript what kind of element to expect, so `input.value?.focus()` and `input.value?.select()` type-check. With version 2.1 or newer of the Vue language tools (the editor extension and `vue-tsc`), the type is inferred from the element that a static `ref="query"` sits on, so you can leave the generic out. Keeping it is harmless and makes the intent visible.

Before 3.5 the pattern was a plain `ref` whose variable name had to match the attribute:

```
// Vue 3.4 and earlier: the variable name must equal the ref attribute
const query = ref<HTMLInputElement | null>(null)
```

Both forms work in 3.5. Use `useTemplateRef`, because the name matching was easy to break during a rename and the new call says what it is.

A ref on a child component gives you the component instance instead of an element. A `<script setup>` component exposes nothing by default, so the child must opt in: say `Dialog.vue` calls `defineExpose({ open })`. The parent puts `ref="dialog"` on `<Dialog>`, types the ref as `useTemplateRef<InstanceType<typeof Dialog>>('dialog')` (the type of a `Dialog` instance) and calls `dialog.value?.open()`.

Reach for this rarely. Props and events are the normal way to talk to a child; an exposed method is for imperative things such as opening a dialog.

## Reach the elements a v-for renders

When `ref` sits on an element inside `v-for`, Vue collects all of them into an array after mount. Type it as an array of elements. This checklist adds a row and focuses its new input:

*components/ChecklistEditor.vue*

```
<script setup lang="ts">
import { nextTick, ref, useTemplateRef } from 'vue'

const items = ref([{ id: 1, text: 'Read the lesson' }])
const inputs = useTemplateRef<HTMLInputElement[]>('inputs')

async function addItem() {
  const id = Date.now()
  items.value.push({ id, text: '' })
  await nextTick()
  inputs.value?.find(el => el.dataset.id === String(id))?.focus()
}
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <input ref="inputs" v-model="item.text" :data-id="item.id">
    </li>
  </ul>
  <button type="button" @click="addItem">Add item</button>
</template>
```

Two details make this work:

- `await nextTick()` waits until Vue has re-rendered. Pushing into `items` schedules the new `<li>` but does not create it immediately, so without the wait the new input is not in the array yet.
- The lookup goes through a `data-id` attribute rather than `inputs.value.at(-1)`, because Vue does not guarantee that the ref array is in the same order as the source array.

## React to a change with watch, not onUpdated

`onUpdated` fires after any re-render of the component, for any reason. It is tempting to use it to react to a change, but it does not tell you what changed, and changing reactive state inside it triggers another update, which can loop.

To run code after a specific piece of data changed and the DOM caught up, `watch` that data with the `{ flush: 'post' }` option, which calls your function after Vue has updated the DOM. Or keep a normal watcher and `await nextTick()` inside it, the same wait `addItem` uses above. Keep `onUpdated` for the rare case where you must inspect the DOM after every render, such as syncing a third-party widget.

## Stop timers and listeners in onUnmounted

Anything you start in `onMounted` that lives outside the component (a timer, a window listener, a subscription) keeps running after the component is gone unless you stop it. A removed component whose `setInterval` is still ticking is a memory leak, and its callback can throw when it reads a template ref that is now `null`.

`Clock.vue` starts a timer and a resize listener on mount and stops both on unmount:

*components/Clock.vue*

```
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'

const now = ref(new Date())
const width = ref(window.innerWidth)
let timer: number | undefined

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

onMounted(() => {
  timer = window.setInterval(() => { now.value = new Date() }, 1000)
  window.addEventListener('resize', onResize)
})

onUnmounted(() => {
  window.clearInterval(timer)
  window.removeEventListener('resize', onResize)
})
</script>

<template>
  <p>{{ now.toLocaleTimeString() }} at {{ width }}px</p>
</template>
```

Reading `window.innerWidth` during setup is fine in a browser-only app: `window` exists before any component, and only the component's own elements wait for mount.

The pairing is the rule: every `setInterval` has a `clearInterval`, every `addEventListener` has a `removeEventListener` with the same function reference, which is why `onResize` is a named function rather than an inline arrow.

Once you have written this pair three times, a library such as VueUse's `useEventListener` and `useIntervalFn` will do the cleanup for you, and it is fine to use them. Understanding the pair first is what lets you spot the leak when a library is not involved.

**Check it stuck**3 questions from the Lifecycle and template refs quiz.

[**Practice now**](https://vuejs.school/learn/beginner/lifecycle-and-template-refs?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 5 min read**Forms and inputs: v-model modifiers, checkboxes and selects**](https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects) [Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-11-04","description":"Learn when onMounted, onUpdated and onUnmounted fire, why DOM work waits for mount, and how useTemplateRef() reaches a real element or a child component.","name":"Lifecycle hooks and template refs, with useTemplateRef()","url":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs","breadcrumb":{"@id":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-11-04","description":"Learn when onMounted, onUpdated and onUnmounted fire, why DOM work waits for mount, and how useTemplateRef() reaches a real element or a child component.","headline":"Lifecycle hooks and template refs, with useTemplateRef()","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCk%2Cdescription_~TGVhcm4gd2hlbiBvbk1vdW50ZWQsIG9uVXBkYXRlZCBhbmQgb25Vbm1vdW50ZWQgZmlyZSwgd2h5IERPTSB3b3JrIHdhaXRzIGZvciBtb3VudCwgYW5kIGhvdyB1c2VUZW1wbGF0ZVJlZigpIHJlYWNoZXMgYSByZWFsIGVsZW1lbnQgb3IgYSBjaGlsZCBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9saWZlY3ljbGUtaG9va3MtYW5kLXRlbXBsYXRlLXJlZnMi%2Cs_tF5SREA7_eDtFXOF.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/lifecycle-hooks-and-template-refs#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Lifecycle hooks and template refs, with useTemplateRef()","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCk%2Cdescription_~TGVhcm4gd2hlbiBvbk1vdW50ZWQsIG9uVXBkYXRlZCBhbmQgb25Vbm1vdW50ZWQgZmlyZSwgd2h5IERPTSB3b3JrIHdhaXRzIGZvciBtb3VudCwgYW5kIGhvdyB1c2VUZW1wbGF0ZVJlZigpIHJlYWNoZXMgYSByZWFsIGVsZW1lbnQgb3IgYSBjaGlsZCBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9saWZlY3ljbGUtaG9va3MtYW5kLXRlbXBsYXRlLXJlZnMi%2Cs_tF5SREA7_eDtFXOF.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TGlmZWN5Y2xlIGhvb2tzIGFuZCB0ZW1wbGF0ZSByZWZzLCB3aXRoIHVzZVRlbXBsYXRlUmVmKCk%2Cdescription_~TGVhcm4gd2hlbiBvbk1vdW50ZWQsIG9uVXBkYXRlZCBhbmQgb25Vbm1vdW50ZWQgZmlyZSwgd2h5IERPTSB3b3JrIHdhaXRzIGZvciBtb3VudCwgYW5kIGhvdyB1c2VUZW1wbGF0ZVJlZigpIHJlYWNoZXMgYSByZWFsIGVsZW1lbnQgb3IgYSBjaGlsZCBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9saWZlY3ljbGUtaG9va3MtYW5kLXRlbXBsYXRlLXJlZnMi%2Cs_tF5SREA7_eDtFXOF.png"}]}
```