Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
250 lines
5.2 KiB
Markdown
250 lines
5.2 KiB
Markdown
---
|
|
title: Template Refs Are Null Until Mounted
|
|
impact: HIGH
|
|
impactDescription: Accessing template ref before mount or after unmount causes runtime errors
|
|
type: gotcha
|
|
tags: [vue3, typescript, template-refs, lifecycle, null-safety]
|
|
---
|
|
|
|
# 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 `| null` union
|
|
- [ ] Only access refs inside `onMounted` or after
|
|
- [ ] Use optional chaining (`?.`) when accessing ref properties
|
|
- [ ] Handle `v-if` scenarios where ref can become null again
|
|
- [ ] Consider using `useTemplateRef` in Vue 3.5+
|
|
|
|
## The Problem
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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`:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<!-- ChildComponent.vue -->
|
|
<script setup lang="ts">
|
|
function exposedMethod() {
|
|
console.log('Called from parent')
|
|
}
|
|
|
|
defineExpose({
|
|
exposedMethod
|
|
})
|
|
</script>
|
|
```
|
|
|
|
## Multiple Refs with v-for
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```typescript
|
|
// 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
|
|
}
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js TypeScript with Composition API - Template Refs](https://vuejs.org/guide/typescript/composition-api.html#typing-template-refs)
|
|
- [Vue.js Template Refs](https://vuejs.org/guide/essentials/template-refs.html)
|