Files
2026-04-17 23:26:01 +00:00

217 lines
4.4 KiB
Markdown

---
title: Component Slots Best Practices
impact: MEDIUM
impactDescription: Poor slot API design causes empty DOM wrappers, weak TypeScript safety, brittle defaults, and unnecessary component overhead
type: best-practice
tags: [vue3, slots, components, typescript, composables]
---
# Component Slots Best Practices
**Impact: MEDIUM** - Slots are a core component API surface in Vue. Structure them intentionally so templates stay predictable, typed, and performant.
## Task List
- Use shorthand syntax for named slots (`#` instead of `v-slot:`)
- Render optional slot wrapper elements only when slot content exists (`$slots` checks)
- Type scoped slot contracts with `defineSlots` in TypeScript components
- Provide fallback content for optional slots
- Prefer composables over renderless components for pure logic reuse
## Shorthand syntax for named slots
**BAD:**
```vue
<MyComponent>
<template v-slot:header> ... </template>
</MyComponent>
```
**GOOD:**
```vue
<MyComponent>
<template #header> ... </template>
</MyComponent>
```
## Conditionally Render Optional Slot Wrappers
Use `$slots` checks when wrapper elements add spacing, borders, or layout constraints.
**BAD:**
```vue
<!-- Card.vue -->
<template>
<article class="card">
<header class="card-header">
<slot name="header" />
</header>
<section class="card-body">
<slot />
</section>
<footer class="card-footer">
<slot name="footer" />
</footer>
</article>
</template>
```
**GOOD:**
```vue
<!-- Card.vue -->
<template>
<article class="card">
<header v-if="$slots.header" class="card-header">
<slot name="header" />
</header>
<section v-if="$slots.default" class="card-body">
<slot />
</section>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
</article>
</template>
```
## Type Scoped Slot Props with defineSlots
In `<script setup lang="ts">`, use `defineSlots` so slot consumers get autocomplete and static checks.
**BAD:**
```vue
<!-- ProductList.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
}
defineProps<{ products: Product[] }>()
</script>
<template>
<ul>
<li v-for="(product, index) in products" :key="product.id">
<slot :product="product" :index="index" />
</li>
</ul>
</template>
```
**GOOD:**
```vue
<!-- ProductList.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
}
defineProps<{ products: Product[] }>()
defineSlots<{
default(props: { product: Product; index: number }): any
empty(): any
}>()
</script>
<template>
<ul v-if="products.length">
<li v-for="(product, index) in products" :key="product.id">
<slot :product="product" :index="index" />
</li>
</ul>
<slot v-else name="empty" />
</template>
```
## Provide Slot Fallback Content
Fallback content makes components resilient when parents omit optional slots.
**BAD:**
```vue
<!-- SubmitButton.vue -->
<template>
<button type="submit" class="btn-primary">
<slot />
</button>
</template>
```
**GOOD:**
```vue
<!-- SubmitButton.vue -->
<template>
<button type="submit" class="btn-primary">
<slot>Submit</slot>
</button>
</template>
```
## Prefer Composables for Pure Logic Reuse
Renderless components are still useful for slot-driven composition, but composables are usually cleaner for logic-only reuse.
**BAD:**
```vue
<!-- MouseTracker.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function onMove(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
</script>
<template>
<slot :x="x" :y="y" />
</template>
```
**GOOD:**
```ts
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function onMove(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
return { x, y }
}
```
```vue
<!-- MousePosition.vue -->
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>{{ x }}, {{ y }}</p>
</template>
```