963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
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
}