Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/ts-template-ref-null-handling.md
1---2title: Template Refs Are Null Until Mounted3impact: HIGH4impactDescription: Accessing template ref before mount or after unmount causes runtime errors5type: gotcha6tags: [vue3, typescript, template-refs, lifecycle, null-safety]7---89# Template Refs Are Null Until Mounted1011**Impact: HIGH** - Template refs have an initial value of `null` and remain null until the component mounts. They can also become null again if the referenced element is removed by `v-if`. Always account for this in TypeScript with union types and optional chaining.1213## Task Checklist1415- [ ] Always type template refs with `| null` union16- [ ] Only access refs inside `onMounted` or after17- [ ] Use optional chaining (`?.`) when accessing ref properties18- [ ] Handle `v-if` scenarios where ref can become null again19- [ ] Consider using `useTemplateRef` in Vue 3.5+2021## The Problem2223```vue24<script setup lang="ts">25import { ref } from 'vue'2627// WRONG: Doesn't account for null28const inputRef = ref<HTMLInputElement>()2930// WRONG: Will crash if accessed before mount31inputRef.value.focus() // Error: Cannot read properties of null3233// WRONG: Accessed in setup, element doesn't exist yet34console.log(inputRef.value.value) // Error!35</script>3637<template>38<input ref="inputRef" />39</template>40```4142## The Solution4344```vue45<script setup lang="ts">46import { ref, onMounted } from 'vue'4748// CORRECT: Include null in the type49const inputRef = ref<HTMLInputElement | null>(null)5051// CORRECT: Access in onMounted when DOM exists52onMounted(() => {53inputRef.value?.focus() // Safe with optional chaining54})5556// CORRECT: Guard before accessing57function focusInput() {58if (inputRef.value) {59inputRef.value.focus()60}61}62</script>6364<template>65<input ref="inputRef" />66</template>67```6869## Vue 3.5+: useTemplateRef7071Vue 3.5 introduces `useTemplateRef` with better type inference:7273```vue74<script setup lang="ts">75import { useTemplateRef, onMounted } from 'vue'7677// Type is automatically inferred for static refs78const inputRef = useTemplateRef<HTMLInputElement>('input')7980onMounted(() => {81inputRef.value?.focus()82})83</script>8485<template>86<input ref="input" />87</template>88```8990## Handling v-if Scenarios9192Refs can become `null` when elements are conditionally rendered:9394```vue95<script setup lang="ts">96import { ref, watch } from 'vue'9798const showModal = ref(false)99const modalRef = ref<HTMLDivElement | null>(null)100101// WRONG: Assuming ref always exists after first mount102function closeModal() {103modalRef.value.classList.remove('open') // May be null!104}105106// CORRECT: Always guard access107function closeModal() {108modalRef.value?.classList.remove('open')109}110111// CORRECT: Watch for ref changes112watch(modalRef, (newRef) => {113if (newRef) {114// Modal element just mounted115newRef.focus()116}117// If null, modal was unmounted118})119</script>120121<template>122<div v-if="showModal" ref="modalRef" class="modal">123Modal content124</div>125</template>126```127128## Component Refs129130For component refs, use `InstanceType`:131132```vue133<script setup lang="ts">134import { ref, onMounted } from 'vue'135import ChildComponent from './ChildComponent.vue'136137// Component ref with null138const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)139140onMounted(() => {141// Access exposed methods/properties142childRef.value?.exposedMethod()143})144</script>145146<template>147<ChildComponent ref="childRef" />148</template>149```150151Remember: Child components must use `defineExpose` to expose methods:152153```vue154<!-- ChildComponent.vue -->155<script setup lang="ts">156function exposedMethod() {157console.log('Called from parent')158}159160defineExpose({161exposedMethod162})163</script>164```165166## Multiple Refs with v-for167168```vue169<script setup lang="ts">170import { ref, onMounted } from 'vue'171172const items = ref(['a', 'b', 'c'])173174// Array of refs for v-for175const itemRefs = ref<(HTMLLIElement | null)[]>([])176177onMounted(() => {178// Access specific item179itemRefs.value[0]?.focus()180181// Iterate safely182itemRefs.value.forEach(el => {183el?.classList.add('mounted')184})185})186</script>187188<template>189<ul>190<li191v-for="(item, index) in items"192:key="item"193:ref="el => { itemRefs[index] = el as HTMLLIElement }"194>195{{ item }}196</li>197</ul>198</template>199```200201## Async Operations and Refs202203Be careful with async operations:204205```vue206<script setup lang="ts">207import { ref, onMounted } from 'vue'208209const containerRef = ref<HTMLDivElement | null>(null)210211onMounted(async () => {212// containerRef.value exists here213214await fetchData()215216// CAREFUL: Component might have unmounted during await217// Always re-check before accessing218if (containerRef.value) {219containerRef.value.scrollTop = 0220}221})222</script>223```224225## Type Guard Pattern226227Create a reusable type guard for cleaner code:228229```typescript230// utils/refs.ts231export function assertRef<T>(232ref: Ref<T | null>,233message = 'Ref is not available'234): asserts ref is Ref<T> {235if (ref.value === null) {236throw new Error(message)237}238}239240// Usage in component241function mustFocus() {242assertRef(inputRef, 'Input element not mounted')243inputRef.value.focus() // TypeScript knows it's not null here244}245```246247## Reference248- [Vue.js TypeScript with Composition API - Template Refs](https://vuejs.org/guide/typescript/composition-api.html#typing-template-refs)249- [Vue.js Template Refs](https://vuejs.org/guide/essentials/template-refs.html)250