Vue3のフォールスルー属性を理解してコンポーネント設計を改善しよう
Vue3のフォールスルー属性を理解してコンポーネント設計を改善しよう
Vue3でコンポーネントを開発していると、親コンポーネントから子コンポーネントに渡される属性の扱いについて悩むことがあります。特に、classやstyle、idなどの属性がどのように処理されるかを理解することは、再利用可能なコンポーネントを作る上で重要です。
この記事では、Vue3のフォールスルー属性(Fallthrough Attributes)について詳しく解説し、実際のコード例を通じて理解を深めていきます。
フォールスルー属性とは
フォールスルー属性とは、コンポーネントに渡される属性やイベントリスナーのうち、propsやemitsで定義されていないものを指します。具体的には以下のような属性が該当します:
classstyleid-
data-*属性 - イベントリスナー(
@clickなど)
これらの属性は、コンポーネントのルート要素に自動的に継承されるという特徴があります。
基本的な動作例
まず、シンプルな例から見てみましょう。
単一ルート要素の場合
<!-- DatePicker.vue -->
<template>
<div class="date-picker">
<input type="datetime-local" />
</div>
</template>
このコンポーネントを以下のように使用すると:
<template>
<DatePicker
class="custom-picker"
data-status="activated"
@click="handleClick"
/>
</template>
レンダリング結果は次のようになります:
<div class="date-picker custom-picker" data-status="activated">
<input type="datetime-local" />
</div>
class、data-status属性、そして@clickイベントリスナーがルート要素であるdivに自動的に継承されています。
複数ルート要素の場合の注意点
Vue3では、コンポーネントが複数のルート要素を持つことができます。この場合、フォールスルー属性は自動的に継承されません。
<!-- MultiRoot.vue -->
<template>
<div>First root</div>
<div>Second root</div>
</template>
このコンポーネントに属性を渡しても、どちらの要素にも継承されません:
<MultiRoot class="custom-class" />
結果:
<div>First root</div>
<div>Second root</div>
$attrsを使用した手動継承
複数ルート要素の場合や、特定の要素に属性を継承したい場合は、$attrsを使用して手動で属性をバインドします。
<!-- CustomButton.vue -->
<template>
<div class="button-wrapper">
<button v-bind="$attrs" class="custom-button">
<slot />
</button>
</div>
</template>
使用例:
<CustomButton
class="primary"
type="submit"
@click="handleSubmit"
>
送信
</CustomButton>
結果:
<div class="button-wrapper">
<button class="custom-button primary" type="submit">
送信
</button>
</div>
inheritAttrs: falseの活用
inheritAttrs: falseを設定することで、ルート要素への自動的な属性継承を無効化できます。これにより、より細かい制御が可能になります。
<!-- CustomInput.vue -->
<template>
<div class="input-wrapper">
<label>{{ label }}</label>
<input
v-bind="$attrs"
class="custom-input"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
label: String,
modelValue: String
},
emits: ['update:modelValue']
}
</script>
使用例:
<CustomInput
label="ユーザー名"
v-model="username"
placeholder="ユーザー名を入力"
class="form-input"
/>
結果:
<div class="input-wrapper">
<label>ユーザー名</label>
<input
class="custom-input form-input"
placeholder="ユーザー名を入力"
/>
</div>
属性の強制変換の変更点
Vue3では、属性の強制変換に関する変更が行われました。真偽値のfalseを属性値としてバインドした場合、属性が削除されず、attr="false"として設定されます。
<!-- Vue2の動作 -->
<MyComponent :disabled="false" />
<!-- 結果: <div></div> (disabled属性なし) -->
<!-- Vue3の動作 -->
<MyComponent :disabled="false" />
<!-- 結果: <div disabled="false"></div> -->
属性を削除するには、nullまたはundefinedを使用します:
<MyComponent :disabled="null" />
<!-- 結果: <div></div> (disabled属性なし) -->
実践的な使用例
1. 再利用可能なボタンコンポーネント
<!-- AppButton.vue -->
<template>
<button
v-bind="$attrs"
:class="buttonClass"
:disabled="disabled"
>
<slot />
</button>
</template>
<script>
export default {
inheritAttrs: false,
props: {
variant: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'secondary', 'danger'].includes(value)
},
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
disabled: Boolean
},
computed: {
buttonClass() {
return [
'app-button',
`app-button--${this.variant}`,
`app-button--${this.size}`,
{ 'app-button--disabled': this.disabled }
]
}
}
}
</script>
使用例:
<AppButton
variant="primary"
size="large"
@click="handleClick"
data-testid="submit-button"
>
送信
</AppButton>
2. フォーム入力コンポーネント
<!-- FormField.vue -->
<template>
<div class="form-field">
<label v-if="label" :for="inputId" class="form-field__label">
{{ label }}
<span v-if="required" class="form-field__required">*</span>
</label>
<input
:id="inputId"
v-bind="$attrs"
:class="inputClass"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
<div v-if="error" class="form-field__error">{{ error }}</div>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
label: String,
modelValue: [String, Number],
error: String,
required: Boolean
},
emits: ['update:modelValue'],
computed: {
inputId() {
return `input-${Math.random().toString(36).substr(2, 9)}`
},
inputClass() {
return [
'form-field__input',
{ 'form-field__input--error': this.error }
]
}
}
}
</script>
まとめ
Vue3のフォールスルー属性を理解することで、以下のようなメリットが得られます:
- 再利用性の向上: コンポーネントがより柔軟に使用できるようになります
- 一貫性の確保: 属性の継承ルールを理解することで、予期しない動作を避けられます
- 保守性の向上: 明示的な属性継承により、コードの意図が明確になります
特に以下のポイントを覚えておきましょう:
- 単一ルート要素では自動的に属性が継承される
- 複数ルート要素では
$attrsを使用して手動で継承する -
inheritAttrs: falseで自動継承を無効化できる - Vue3では属性の強制変換の動作が変更されている
フォールスルー属性を適切に活用して、より良いVue3コンポーネントを設計していきましょう!
Discussion
useAttrs() を使って $attrs を複数のコンポーネントに分割するのも手ですね。