LoginSignup
24
12

More than 3 years have passed since last update.

React の {...props} は Vue ではこう書く

Last updated at Posted at 2019-12-25

❓問題

ラッパーコンポーネントを作るとき, React の場合は第1引数の props を展開するだけですべてリレーすることができますが, Vue では

  • $props
  • $attrs
  • $listeners
  • $scopedSlots

がすべて別概念になっているため,1つ1つ対応する必要があります。忘れやすいところなので備忘録的に書いておきます。なおこの記事は,以下の記事における内容を理解していることを前提とします。

Special Thanks: @gaogao_9

💡解決策

シンプルな例

React

childrenprops に含まれているため,何も深く考えずにリレーすることができます。

すべてリレー
export default function WrappedButton(props) {
  return <Button {...props} />
}

分割代入を利用すると, props から特定のキーを除外することができます。

一部を除外してリレー
export default function WrappedButton({ foo, bar, ...props }) {
  return <Button {...props} />
}

明示的に children を除外した場合は,自分でそこに値を与えます。

children 以外をリレー
export default function WrappedButton({ children, ...props }) {
  return <Button {...props}>Hello</Button>
}

Vue

$attrs $listeners $scopedSlots をすべて受け流す一般形です。 スロットが特に厄介ですね。初見でこれを覚えるのは無理…

すべてリレー
<template>
  <button v-bind="$attrs" v-on="$listeners">
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </button>
</template>

<script>
  export default {
    inheritAttrs: false,
  }
</script>

React で分割代入によって退避していたものは, Vue では $props に退避させることで実現されます。

props で指定されたものを $props に退避し,残りの $attrs をリレー
<template>
  <button v-bind="$attrs" v-on="$listeners">
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </button>
</template>

<script>
  export default {
    inheritAttrs: false,
    props: {
      foo: {
        type: String,
      },
      bar: {
        type: Number,
      },
    },
  }
</script>

スロットをリレーしない場合はシンプルになります。

スロット以外をリレー
<template>
  <button v-bind="$attrs" v-on="$listeners">
    Hello
  </button>
</template>

<script>
  export default {
    inheritAttrs: false,
  }
</script>

実用的な例

Vue.js の inheritAttrs に関する大きな勘違い - Qiita で登場した <DissmissibleButton> をよりお行儀よく書いてみます。

React

React は何も工夫しないと, コンポーネントで明示的に割り当てた onClick{...props} に含まれる onClick が衝突してしまうので,これを避けるために1つに統合する必要があります。

export default function DismissibleButton({
  timeout,
  onClick,
  ...props,
}) {
  const [visible, setVisible] = useState(true)

  const hide = useCallback((e) => {
    setTimeout(() => {
      setVisible(false)
    }, timeout)

    // もとの onClick も実行する
    onClick(e)
  }, [onClick, timeout])

  return visible ? <VBtn onClick={hide} {...props} /> : null
}

Vue

Vue は v-on:click@click) で指定されたものを v-on="$listeners" の中に含まれる click で上書きしません。 どちらもクリック時に実行されます。

<template>
  <v-btn v-if="visible" @click="hide" v-bind="$attrs" v-on="$listeners">
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </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>

✨まとめ

  • React は純粋に JavaScript を書いている感覚で JSX を書けば OK。 JSX はあくまで JavaScript のシンタックスシュガー。
    • props にスロット(children)もリスナーもすべて含まれるし,分割代入しない限りは区分も発生しない。
    • イベントハンドラの上書きに注意し,自分で責任を持って統合する。
  • Vue は JavaScript とは別のテンプレートエンジン的なものであることを意識する。
    • $props $attrs $listeners $scopedSlots すべてが別個なので注意。とくにスロット($scopedSlots)は独特の覚えにくい記法を使用するので,完全に覚えるまでは間違えないようにコピペする。
    • イベントハンドラは上書きされないため,コンポーネント側で定義したハンドラと上から渡されたハンドラの衝突を気にする必要はない。
24
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
12