skip to content

A Vue 3 component's template ref reads null exactly when code needs the element; what usually causes that, and how do you fix each cause?

level: seniorimportance: should knowfreq 45%

answer

  1. read before the first render
  2. branch not rendered yet
  3. state changed, DOM not yet
  4. key or name mismatch
  5. plain variable, not a ref

basics

~20 s

A template ref is null before mount, while its element sits in an unrendered v-if branch, right after state changes but before the re-render, or when the ref key or name does not match. Read it later, await nextTick(), or fix the link.

solid answer

~40 s

Work through the timeline. The ref is `null` during `setup` and the first render, so DOM calls belong in `onMounted` or handlers. It is `null` whenever its element is in a `v-if` branch that is not rendered, and it goes back to `null` when the branch unmounts. After you flip `showSearch.value = true`, the element does not exist until Vue's batched re-render, so `await nextTick()` before reading it. Then check the link: `useTemplateRef('search')` must use the same string as `ref="search"`, a pre-3.5 same-named ref must match by variable name, and a plain `let` variable does not receive the element in production builds (development builds warn about exactly that). For a `<script setup>` child, a non-null ref can still lack methods until `defineExpose` publishes them.

code

vue · 18 lines
vue
<script setup>
import { ref, useTemplateRef, nextTick } from 'vue'

const showSearch = ref(false)
const searchEl = useTemplateRef('search')

async function openSearch() {
  showSearch.value = true
  console.log(searchEl.value) // null: not re-rendered yet
  await nextTick()
  searchEl.value?.focus()
}
</script>

<template>
  <button @click="openSearch">Search</button>
  <input v-if="showSearch" ref="search" type="search" />
</template>

go deeper

for a junior

Remember that refs are only filled after mount and when their element is rendered.

for a middle

Explain batched re-rendering and why nextTick() is needed after revealing an element.

for a senior

Walk the timeline systematically, covering render timing, v-if branches, key mismatches and component refs.

for a principal

Reduce this class of bug by setting conventions for guarded imperative helpers and exposed component methods.

## Start from the timeline A template ref is filled by Vue **after** the element is rendered and cleared when the element is unmounted. Most "ref is null" bugs are code that reads the ref at a moment when no element exists. The scenario in interviews is usually: click a button that reveals a search box, then focus it, and nothing happens. ## Cause 1: reading before the first render During `setup`, and while the first render runs, the ref is `null`. Code at the top level of `<script setup>` or a template expression that dereferences it on the first render fails. - **Fix:** move DOM work into `onMounted`, or into handlers that can only run after mount. ## Cause 2: the element is not rendered If the element is inside `v-if="showSearch"` and `showSearch` is `false`, there is no element, so the ref is `null`. When the branch is removed later, Vue sets the ref back to `null`. - **Fix:** guard reads (`searchEl.value?.focus()`), or use `v-show` when you need the element to exist while hidden, accepting that it stays in the DOM. ## Cause 3: reading right after a state change Vue batches DOM updates. Setting `showSearch.value = true` schedules a re-render; it does not create the element synchronously. Reading the ref on the next line still gives `null`. ```js async function openSearch() { showSearch.value = true await nextTick() // wait for the re-render searchEl.value?.focus() // the input now exists } ``` - **Fix:** `await nextTick()` after the state change, then read the ref. ## Cause 4: the link between script and template is wrong | Setup | Template | Result | |---|---|---| | `useTemplateRef('search')` | `ref="search"` | works | | `useTemplateRef('search')` | `ref="searchInput"` | stays `null`: keys differ | | `const search = ref(null)` | `ref="search"` | works (pre-3.5 style) | | `let search = null` | `ref="search"` | dev warning; does not work in production | The `useTemplateRef` key is matched as a string, so a typo produces no compile error, only a ref that never fills. With the pre-3.5 style the variable **name** is the link, so renaming the variable breaks it silently. Calling `useTemplateRef` twice with the same key logs a development warning that the key already exists. - **Fix:** keep the key in one place, and prefer `useTemplateRef` so the variable name can change freely. ## Cause 5: the ref is on a component, not an element On a component, the ref holds the child's public instance, not a DOM node. `childRef.value.focus` is `undefined` unless the child exposes a `focus` method through `defineExpose`; for `<script setup>` children, nothing else is visible. - **Fix:** expose an intention-revealing method from the child and call that. ## A quick checklist 1. Is the read happening after mount? 2. Is the element's `v-if` branch actually rendered right now? 3. Did state just change without waiting for the re-render? 4. Do the key or variable name and the attribute match exactly? 5. Is the ref on a component whose API is not exposed? ## Preventing it - Wrap imperative access in small functions that guard `null`. - Keep refs close to the element they target; avoid passing them through several layers. - When code reacts to a ref appearing, handle both the element and `null`, because the ref toggles as branches mount and unmount. ## Why Vue batches the re-render Cause 3 surprises people because the state change looks immediate. Vue collects every state change made in the same tick and re-renders each affected component **once**, in a queued job, instead of patching the DOM after every assignment. That is what keeps ten assignments in a handler from producing ten DOM updates. The price is that code which changes state and then touches the DOM must wait for the queued render: `await nextTick()` resolves after that flush, at which point the new element exists and its template ref has been assigned.

  • In Vue 3, would switching the search box from `v-if` to `v-show` remove the need for `nextTick()` before focusing it?
    The ref would be filled from mount on, because a `v-show` element is always rendered. But focusing an element still hidden with `display: none` does nothing, and `display` is only updated on the re-render, so you still wait for `nextTick()` after flipping the flag.
  • Why does a typo in the `useTemplateRef` key not produce a compile error in a plain JavaScript Vue 3 SFC?
    The key is an ordinary string matched at runtime against the ref attribute values. Nothing checks at build time that a matching `ref` exists, so the ref simply stays `null` with no error. Keep each key string in one place, and when a ref is always `null`, compare the key and the attribute first.

saying these in an interview costs you the question

  • Expects the element to exist right after setting the v-if flag
  • Reads template refs at the top level of script setup
  • Believes a useTemplateRef key typo fails compilation
  • Uses a plain let variable as the ref target
  • Calls DOM methods on a component ref as if it were an element