Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Enforce Vue 3 best practices—Composition API, script setup, TypeScript, component boundaries, and reactivity patterns
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/component-fallthrough-attrs.md
1---2title: Component Fallthrough Attributes Best Practices3impact: MEDIUM4impactDescription: Incorrect $attrs access and reactivity assumptions can cause undefined values and watchers that never run5type: best-practice6tags: [vue3, attrs, fallthrough-attributes, composition-api, reactivity]7---89# Component Fallthrough Attributes Best Practices1011**Impact: MEDIUM** - Fallthrough attributes are straightforward once you follow Vue's conventions: hyphenated names use bracket notation, listener keys are camelCase `onX`, and `useAttrs()` is current-but-not-reactive.1213## Task List1415- Access hyphenated attribute names with bracket notation (for example `attrs['data-testid']`)16- Access event listeners with camelCase `onX` keys (for example `attrs.onClick`)17- Do not `watch()` values returned from `useAttrs()`; those watchers do not trigger on attr changes18- Use `onUpdated()` for attr-driven side effects19- Promote frequently observed attrs to props when reactive observation is required2021## Access Attribute and Listener Keys Correctly2223Hyphenated attribute names preserve their original casing in JavaScript, so dot notation does not work for keys that include `-`.2425**BAD:**26```vue27<script setup>28import { useAttrs } from 'vue'2930const attrs = useAttrs()3132console.log(attrs.data-testid) // Syntax error33console.log(attrs.dataTestid) // undefined for data-testid34console.log(attrs['on-click']) // undefined35console.log(attrs['@click']) // undefined36</script>37```3839**GOOD:**40```vue41<script setup>42import { useAttrs } from 'vue'4344const attrs = useAttrs()4546console.log(attrs['data-testid'])47console.log(attrs['aria-label'])48console.log(attrs['foo-bar'])4950console.log(attrs.onClick)51console.log(attrs.onCustomEvent)52console.log(attrs.onMouseEnter)53</script>54```5556### Naming Reference5758| Parent Usage | Access in `attrs` |59|--------------|-------------------|60| `class="foo"` | `attrs.class` |61| `data-id="123"` | `attrs['data-id']` |62| `aria-label="..."` | `attrs['aria-label']` |63| `foo-bar="baz"` | `attrs['foo-bar']` |64| `@click="fn"` | `attrs.onClick` |65| `@custom-event="fn"` | `attrs.onCustomEvent` |66| `@update:modelValue="fn"` | `attrs['onUpdate:modelValue']` |6768## `useAttrs()` Is Not Reactive6970`useAttrs()` always reflects the latest values, but it is intentionally not reactive for watcher tracking.7172**BAD:**73```vue74<script setup>75import { watch, watchEffect, useAttrs } from 'vue'7677const attrs = useAttrs()7879watch(80() => attrs.someAttr,81(newValue) => {82console.log('Changed:', newValue) // Never runs on attr changes83}84)8586watchEffect(() => {87console.log(attrs.class) // Runs on setup, not on attr updates88})89</script>90```9192**GOOD:**93```vue94<script setup>95import { onUpdated, useAttrs } from 'vue'9697const attrs = useAttrs()9899onUpdated(() => {100console.log('Latest attrs:', attrs)101})102</script>103```104105**GOOD:**106```vue107<script setup>108import { watch } from 'vue'109110const props = defineProps({111someAttr: String112})113114watch(115() => props.someAttr,116(newValue) => {117console.log('Changed:', newValue)118}119)120</script>121```122123## Common Patterns124125### Check for optional attrs safely126127```vue128<script setup>129import { computed, useAttrs } from 'vue'130131const attrs = useAttrs()132133const hasTestId = computed(() => 'data-testid' in attrs)134const ariaLabel = computed(() => attrs['aria-label'] ?? 'Default label')135</script>136```137138### Forward listeners after internal logic139140```vue141<script setup>142import { useAttrs } from 'vue'143144defineOptions({ inheritAttrs: false })145146const attrs = useAttrs()147148function handleClick(event) {149console.log('Internal handling first')150attrs.onClick?.(event)151}152</script>153154<template>155<button @click="handleClick">156<slot />157</button>158</template>159```160161## TypeScript Notes162163`useAttrs()` is typed as `Record<string, unknown>`, so cast individual keys when needed.164165```vue166<script setup lang="ts">167import { useAttrs } from 'vue'168169const attrs = useAttrs()170171const testId = attrs['data-testid'] as string | undefined172const onClick = attrs.onClick as ((event: MouseEvent) => void) | undefined173</script>174```175