Fallthrough Attributes - コンポーネント外部属性の着地点

ロードマップ: Vue.js学習ロードマップ

What(何についてか)

コンポーネントに渡された属性のうち、propsemits として明示的に受け取っていないものが、どこに適用されるかを扱う。典型例は classstyleidv-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.attrsuseAttrs() で参照
自動継承停止inheritAttrs: false を options に記述defineOptions({ inheritAttrs: false })script setup 内に記述
属性の着地点制御template で v-bind="$attrs"同じ。Composition API でも template 主体
reactivity$attrs は反応的ではないuseAttrs() でも同様に反応的ではない

Key Concepts

用語説明
fallthrough attributesprops / 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 に昇格させる。
  • $attrsuseAttrs() は名前から汎用的な属性 API に見えるが、実態は「未消費属性へのアクセス API」であり、その意味を理解して使う必要がある。