Vue.js の inheritAttrs に関する大きな勘違い

# はじめに

Vue コンポーネントには `inheritAttrs` というディレクティブがあります。

- [API — Vue.js #inheritAttrs](https://jp.vuejs.org/v2/api/index.html#inheritAttrs)

直訳すると **「属性を引き継ぐ」**。

まず簡単な例を示します。[Vuetify.js](https://vuetifyjs.com/ja/) の [v-btn](https://vuetifyjs.com/ja/components/buttons) をラップしたボタンを作ってみましょう。

- クリックすると消滅する `DismissibleButton` を作ります。
- 消滅するまでの時間をミリ秒で `timeout` で指定できるようにします。

```vue:DismissibleButton.vue
<template>
  <v-btn v-if="visible" @click="hide">
    <slot />
  </v-btn>
</template>

<script>
  export default {
    props: {
      timeout: {
        type: Number,
        default: 0,
      },
    },
    data() {
      return {
        visible: true,
      };
    },
    methods: {
      hide() {
        setTimeout(() => {
          this.visible = false;
        }, this.timeout);
      },
    },
  };
</script>
```

```vue:使用側
<dismissible-button :timeout="1000">Click Me</dismissible-button>
```

- スロットとしてボタンテキストを受け取り，それをそのまま `<v-btn>` に受け流し。
- 可視性の制御を加えるちょっとした実装を書きました。

# `inheritAttrs` の動作

ではここで， 

**「`<v-btn>` に `to` プロパティと `nuxt` プロパティを渡したい！」**

というニーズが発生したとします。

- [Nuxt × Vuetifyの状況下でv-btnをnuxt-linkにして使いたい - Qiita](https://qiita.com/checche/items/810a71d8f1f93b251aae)

ここで挙げられている解決策の

```vue
<v-btn to="/path/to/link" nuxt>リンク</v-btn>
```

これですね。これをラップした `<dismissible-button>` において，**できるだけ再利用性の高い形で** `<v-btn>` に受け流します。

```vue
<dismissible-button :timeout="1000" to="/path/to/link" nuxt>リンク</dismissible-button>
```

このような使い方をすることを考えます。

## `true` のとき （デフォルト）

何も指定していないときはこれが適用されます。

- [Vue.jsの$attrsとinheritAttrs - isoppp.com](https://isoppp.com/note/2018-12-16/what-is-vue-attrs/)

> 上記の場合 component B は展開されると
> 
> ```html
> <div color="red" type="number">{ color: 'red', type: 'number' }</div>
> ```
> 
> という html を出力します。
>
> component B に props が定義されていない属性を与えるとデフォルトでは生成されるルートの要素に属性が追加されます。

これを読んだ僕は，このようなレンダリング結果を期待しました。 

**`<v-btn>` がコンポーネントのルート要素だから，勝手にそこに引き継いでくれる！！！**

と。

```vue:期待した結果
<template>
  <v-btn v-if="visible" @click="hide" :to="to" :nuxt="nuxt">
    <slot />
  </v-btn>
</template>
```

でも実際はそこに展開されず，なんと **`<v-btn>` より更に下のHTML要素の `<button>`** に渡っていまっていました…

```html:実際の結果
<button to="/path/to/link" nuxt=""></button>
```

こういうことなのだ…
（この図を目に焼き付けて帰ってください）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25060/fd9221fa-e1cd-bb50-5efa-f08417bfc580.png" alt="勘違い" width="800">

端的に言葉で表現すると

**「`inheritAttrs` で下位コンポーネントに流すと『`props` として定義されていたら `$props` に流す』という処理をスキップして全部 `$attrs` に流す」**

ということになります。これは初見殺しだ…

## `false` のとき

`inheritAttrs: false` にすると，暗黙的な受け流しは行われなくなります。**明示的に `v-bind` で受け流した場合は `$props` に流すかどうかの判定が行われますが，**自分でその処理を書く必要があります。

```vue:DismissibleButton.vue
<template>
  <v-btn v-if="visible" @click="hide" v-bind="$attrs"> <!-- ← これを追加！ -->
    <slot />
  </v-btn>
</template>

<script>
  export default {
    inheritAttrs: false, // ← これを追加！
    props: {
      timeout: {
        type: Number,
        default: 0,
      },
    },
    data() {
      return {
        visible: true,
      };
    },
    methods: {
      hide() {
        setTimeout(() => {
          this.visible = false;
        }, this.timeout);
      },
    },
  };
</script>
```

`v-bind="$attrs"` に関して説明すると，以下の2つの記述は等価となります。

```vue
<v-btn v-if="visible" @click="hide" v-bind="$attrs">
```

```vue
<v-btn v-if="visible" @click="hide" :to="$attrs.to" :nuxt="$attrs.nuxt">
```

実際には `$attrs` はこれに限らないので，数が増えても自動的にすべて受け流される `v-bind="$attrs"` が優秀だと言えますね。

# まとめ

大事なことなのでもう一度。`

- **`inheritAttrs` はすべて `$attrs` に流す**
- **明示的に `v-bind="$attrs"` で流すと `$props` と `$attrs` の振り分けが行われる**

基本的に `inheritAttrs: true` にはあまり頼らないほうがいいのかもしれませんね…