5.3 KiB
5.3 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||
|---|---|---|---|---|---|---|---|---|---|
| Template Refs Are Null Until Mounted | HIGH | Accessing template ref before mount or after unmount causes runtime errors | gotcha |
|
Template Refs Are Null Until Mounted
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.
Task Checklist
- Always type template refs with
| nullunion - Only access refs inside
onMountedor after - Use optional chaining (
?.) when accessing ref properties - Handle
v-ifscenarios where ref can become null again - Consider using
useTemplateRefin Vue 3.5+
The Problem
<script setup lang="ts">
import { ref } from 'vue';
// WRONG: Doesn't account for null
const inputRef = ref<HTMLInputElement>();
// WRONG: Will crash if accessed before mount
inputRef.value.focus(); // Error: Cannot read properties of null
// WRONG: Accessed in setup, element doesn't exist yet
console.log(inputRef.value.value); // Error!
</script>
<template>
<input ref="inputRef" />
</template>
The Solution
<script setup lang="ts">
import { ref, onMounted } from 'vue';
// CORRECT: Include null in the type
const inputRef = ref<HTMLInputElement | null>(null);
// CORRECT: Access in onMounted when DOM exists
onMounted(() => {
inputRef.value?.focus(); // Safe with optional chaining
});
// CORRECT: Guard before accessing
function focusInput() {
if (inputRef.value) {
inputRef.value.focus();
}
}
</script>
<template>
<input ref="inputRef" />
</template>
Vue 3.5+: useTemplateRef
Vue 3.5 introduces useTemplateRef with better type inference:
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue';
// Type is automatically inferred for static refs
const inputRef = useTemplateRef<HTMLInputElement>('input');
onMounted(() => {
inputRef.value?.focus();
});
</script>
<template>
<input ref="input" />
</template>
Handling v-if Scenarios
Refs can become null when elements are conditionally rendered:
<script setup lang="ts">
import { ref, watch } from 'vue';
const showModal = ref(false);
const modalRef = ref<HTMLDivElement | null>(null);
// WRONG: Assuming ref always exists after first mount
function closeModal() {
modalRef.value.classList.remove('open'); // May be null!
}
// CORRECT: Always guard access
function closeModal() {
modalRef.value?.classList.remove('open');
}
// CORRECT: Watch for ref changes
watch(modalRef, (newRef) => {
if (newRef) {
// Modal element just mounted
newRef.focus();
}
// If null, modal was unmounted
});
</script>
<template>
<div v-if="showModal" ref="modalRef" class="modal">Modal content</div>
</template>
Component Refs
For component refs, use InstanceType:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';
// Component ref with null
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null);
onMounted(() => {
// Access exposed methods/properties
childRef.value?.exposedMethod();
});
</script>
<template>
<ChildComponent ref="childRef" />
</template>
Remember: Child components must use defineExpose to expose methods:
<!-- ChildComponent.vue -->
<script setup lang="ts">
function exposedMethod() {
console.log('Called from parent');
}
defineExpose({
exposedMethod,
});
</script>
Multiple Refs with v-for
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const items = ref(['a', 'b', 'c']);
// Array of refs for v-for
const itemRefs = ref<(HTMLLIElement | null)[]>([]);
onMounted(() => {
// Access specific item
itemRefs.value[0]?.focus();
// Iterate safely
itemRefs.value.forEach((el) => {
el?.classList.add('mounted');
});
});
</script>
<template>
<ul>
<li
v-for="(item, index) in items"
:key="item"
:ref="
(el) => {
itemRefs[index] = el as HTMLLIElement;
}
"
>
{{ item }}
</li>
</ul>
</template>
Async Operations and Refs
Be careful with async operations:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const containerRef = ref<HTMLDivElement | null>(null);
onMounted(async () => {
// containerRef.value exists here
await fetchData();
// CAREFUL: Component might have unmounted during await
// Always re-check before accessing
if (containerRef.value) {
containerRef.value.scrollTop = 0;
}
});
</script>
Type Guard Pattern
Create a reusable type guard for cleaner code:
// utils/refs.ts
export function assertRef<T>(
ref: Ref<T | null>,
message = 'Ref is not available',
): asserts ref is Ref<T> {
if (ref.value === null) {
throw new Error(message);
}
}
// Usage in component
function mustFocus() {
assertRef(inputRef, 'Input element not mounted');
inputRef.value.focus(); // TypeScript knows it's not null here
}