Fallthrough Attributes - コンポーネント外部属性の着地点
ロードマップ: Vue.js学習ロードマップ
What(何についてか)
コンポーネントに渡された属性のうち、props や emits として明示的に受け取っていないものが、どこに適用されるかを扱う。典型例は class、style、id、v-on リスナである。
Why(なぜ必要か)
薄い UI ラッパーコンポーネントでは、親が付けた class や @click を子の実 DOM に自然に届かせたい。一方で、ラッパー要素や複数 root を持つコンポーネントでは、どこに着地させるかを明示しないと API が曖昧になる。そのため、この仕様はコンポーネント境界設計そのものに関わる。
How(どう動くか)
Attribute Inheritance
fallthrough attribute とは、コンポーネントに渡されたが、そのコンポーネントで props / emits として宣言されていない属性である。
<!-- Parent -->
<MyButton class="large" /><!-- MyButton.vue -->
<template>
<button>Click Me</button>
</template>この場合 class="large" は button に自動付与され、最終的には次の DOM になる。
<button class="large">Click Me</button>単一 root 要素であれば、Vue は未消費属性の着地点を一意に決められる。
class and style Merging
親から渡された class / style は、子の root 要素にもともと書かれている値とマージされる。
<template>
<button class="btn">Click Me</button>
</template><MyButton class="large" />結果:
<button class="btn large">Click Me</button>これにより、子は部品としてのベーススタイルを持ち、親は文脈依存の追加スタイルを重ねられる。
v-on Listener Inheritance
@click などの未宣言イベントリスナも、単一 root 要素へ自動継承される。
<MyButton @click="onClick" /><template>
<button @click="handleInnerClick">Click Me</button>
</template>この場合、親の onClick と子の handleInnerClick は両方発火する。
Nested Component Inheritance
root が別コンポーネントである場合も、未消費属性はさらに下位へ転送される。
<!-- MyButton.vue -->
<template>
<BaseButton />
</template><!-- BaseButton.vue -->
<template>
<button>Click Me</button>
</template><MyButton class="large" /> の class は、最終的に BaseButton の root 要素である button まで到達する。ただし途中のコンポーネントで props / emits として消費された属性は、それ以上 fallthrough しない。
Disabling Attribute Inheritance
root へ自動継承したくない場合は inheritAttrs: false を使う。
<script setup>
defineOptions({
inheritAttrs: false
})
</script>
<template>
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">Click Me</button>
</div>
</template>これは「属性継承を止める」というより、「属性の着地点を自分で決める」ためのモードである。$attrs には props / emits として消費されなかった属性が入る。
Attribute Inheritance on Multiple Root Nodes
複数 root ノードを持つ場合、Vue は自動ではどこに属性を付けるべきか判断できない。
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>この場合に親が属性を渡すと warning が出る。明示的に v-bind="$attrs" を使えば warning を抑制できる。
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>この仕様から、外部 API を素直にしたいコンポーネントは単一 root の方が有利であると読み取れる。
Accessing Fallthrough Attributes in JavaScript
fallthrough attributes は JavaScript からも取得できる。
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>通常の setup() では ctx.attrs、Options API では this.$attrs でも参照できる。
重要なのは、attrs は最新値を反映するが reactive object ではない 点である。watch() や watchEffect() で追跡する前提の API ではない。反応性が必要な値は props として明示的に受ける方が適切である。
Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| 属性アクセス | this.$attrs, setup(props, ctx) の ctx.attrs | useAttrs() で参照 |
| 自動継承停止 | inheritAttrs: false を options に記述 | defineOptions({ inheritAttrs: false }) で script setup 内に記述 |
| 属性の着地点制御 | template で v-bind="$attrs" | 同じ。Composition API でも template 主体 |
| reactivity | $attrs は反応的ではない | useAttrs() でも同様に反応的ではない |
Key Concepts
| 用語 | 説明 |
|---|---|
| fallthrough attributes | props / emits で宣言されていない外部属性 |
| root 要素 | template の最外層にある要素。単一 root なら自動継承の着地点になる |
$attrs | 未消費属性の集合。template や Options API から参照できる |
useAttrs() | script setup / Composition API で $attrs 相当を参照する API |
inheritAttrs: false | 自動継承を止め、属性の適用先を手動制御する設定 |
全体像
flowchart TD P["Parent passes class / style / id / listeners"] --> C["Component boundary"] C -->|single root| R["Root element gets attrs automatically"] C -->|inheritAttrs: false| A["Developer chooses target with v-bind '$attrs'"] C -->|multiple roots| M["No automatic target, explicit binding required"]
実務上の判断軸
- 外部から
classや@clickを自然に受けたい部品は、単一 root の方が API を素直に設計しやすい。 - ラッパー要素が必要な場合は
inheritAttrs: falseを使い、実際に属性を持たせたい要素へv-bind="$attrs"する。 $attrs/useAttrs()は便利だが、コンポーネントの主要データ受け渡しには使わない。反応的な契約が必要ならpropsに昇格させる。$attrsとuseAttrs()は名前から汎用的な属性 API に見えるが、実態は「未消費属性へのアクセス API」であり、その意味を理解して使う必要がある。