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

# ❓問題

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

- `$props`
- `$attrs`
- `$listeners`
- `$scopedSlots`

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

- [Vue.js の inheritAttrs に関する大きな勘違い - Qiita](https://qiita.com/mpyw/items/d3dc2745b6808b0158c3)

**Special Thanks: [@gaogao_9](https://twitter.com/gaogao_9)**

# 💡解決策

## シンプルな例

### React

`children` は `props` に含まれているため，何も深く考えずにリレーすることができます。

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

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

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

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

```jsx:children&#32;以外をリレー
export default function WrappedButton({ children, ...props }) {
  return <Button {...props}>Hello</Button>
}
```

### Vue

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

```vue:<ins><strong>すべてリレー</strong></ins>
<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` に退避させることで実現されます。

```vue:<code>props</code>&#32;で指定されたものを&#32;<code>$props</code>&#32;に退避し，残りの&#32;<code>$attrs</code>&#32;をリレー
<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>
```

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

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

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

## 実用的な例

[Vue.js の inheritAttrs に関する大きな勘違い - Qiita](https://qiita.com/mpyw/items/d3dc2745b6808b0158c3) で登場した `<DissmissibleButton>` をよりお行儀よく書いてみます。

### React

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

```jsx
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` で上書きしません**。 どちらもクリック時に実行されます。

```vue
<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`）は独特の覚えにくい記法を使用するので，完全に覚えるまでは間違えないようにコピペする。
  - イベントハンドラは上書きされないため，コンポーネント側で定義したハンドラと上から渡されたハンドラの衝突を気にする必要はない。