Help us understand the problem. What is going on with this article?

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

はじめに

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

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

まず簡単な例を示します。Vuetify.jsv-btn をラップしたボタンを作ってみましょう。

  • クリックすると消滅する DismissibleButton を作ります。
  • 消滅するまでの時間をミリ秒で timeout で指定できるようにします。
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>
使用側
<dismissible-button :timeout="1000">Click Me</dismissible-button>
  • スロットとしてボタンテキストを受け取り,それをそのまま <v-btn> に受け流し。
  • 可視性の制御を加えるちょっとした実装を書きました。

inheritAttrs の動作

ではここで,

<v-btn>to プロパティと nuxt プロパティを渡したい!」

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

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

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

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

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

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

true のとき (デフォルト)

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

上記の場合 component B は展開されると

<div color="red" type="number">{ color: 'red', type: 'number' }</div>

という html を出力します。

component B に props が定義されていない属性を与えるとデフォルトでは生成されるルートの要素に属性が追加されます。

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

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

と。

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

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

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

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

勘違い

端的に言葉で表現すると

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

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

false のとき

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

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つの記述は等価となります。

<v-btn v-if="visible" @click="hide" v-bind="$attrs">
<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 にはあまり頼らないほうがいいのかもしれませんね…

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした