❓問題
ラッパーコンポーネントを作るとき, React の場合は第1引数の props
を展開するだけですべてリレーすることができますが, Vue では
$props
$attrs
$listeners
$scopedSlots
がすべて別概念になっているため,1つ1つ対応する必要があります。忘れやすいところなので備忘録的に書いておきます。なおこの記事は,以下の記事における内容を理解していることを前提とします。
Special Thanks: @gaogao_9
💡解決策
シンプルな例
React
children
は props
に含まれているため,何も深く考えずにリレーすることができます。
export default function WrappedButton(props) {
return <Button {...props} />
}
分割代入を利用すると, props
から特定のキーを除外することができます。
export default function WrappedButton({ foo, bar, ...props }) {
return <Button {...props} />
}
明示的に 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
)は独特の覚えにくい記法を使用するので,完全に覚えるまでは間違えないようにコピペする。 - イベントハンドラは上書きされないため,コンポーネント側で定義したハンドラと上から渡されたハンドラの衝突を気にする必要はない。
-