154 lines
4.1 KiB
Markdown
154 lines
4.1 KiB
Markdown
|
|
---
|
||
|
|
title: Consider Composables Instead of Renderless Components
|
||
|
|
impact: MEDIUM
|
||
|
|
impactDescription: Renderless slot components add overhead compared to composables for pure logic reuse
|
||
|
|
type: best-practice
|
||
|
|
tags: [vue3, slots, renderless-components, composables, performance, composition-api]
|
||
|
|
---
|
||
|
|
|
||
|
|
# Consider Composables Instead of Renderless Components
|
||
|
|
|
||
|
|
**Impact: MEDIUM** - Renderless components (components that only expose logic via scoped slots without rendering their own UI) were a popular pattern in Vue 2 for logic reuse. In Vue 3, Composition API composables are often more efficient and simpler for the same use case.
|
||
|
|
|
||
|
|
## Task Checklist
|
||
|
|
|
||
|
|
- [ ] Evaluate if a composable can replace a renderless component
|
||
|
|
- [ ] Use renderless components when slot-based composition is genuinely needed
|
||
|
|
- [ ] Prefer composables for pure logic/state sharing
|
||
|
|
- [ ] Consider component overhead vs function overhead
|
||
|
|
|
||
|
|
**Renderless Component Pattern:**
|
||
|
|
```vue
|
||
|
|
<!-- MouseTracker.vue - Renderless component -->
|
||
|
|
<script setup>
|
||
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||
|
|
|
||
|
|
const x = ref(0)
|
||
|
|
const y = ref(0)
|
||
|
|
|
||
|
|
function update(event) {
|
||
|
|
x.value = event.pageX
|
||
|
|
y.value = event.pageY
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => window.addEventListener('mousemove', update))
|
||
|
|
onUnmounted(() => window.removeEventListener('mousemove', update))
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<!-- Only renders the slot, no DOM of its own -->
|
||
|
|
<slot :x="x" :y="y" />
|
||
|
|
</template>
|
||
|
|
```
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- Usage -->
|
||
|
|
<MouseTracker v-slot="{ x, y }">
|
||
|
|
<div>Mouse: {{ x }}, {{ y }}</div>
|
||
|
|
</MouseTracker>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Composable Alternative (Recommended):**
|
||
|
|
```typescript
|
||
|
|
// composables/useMouse.ts
|
||
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||
|
|
|
||
|
|
export function useMouse() {
|
||
|
|
const x = ref(0)
|
||
|
|
const y = ref(0)
|
||
|
|
|
||
|
|
function update(event: MouseEvent) {
|
||
|
|
x.value = event.pageX
|
||
|
|
y.value = event.pageY
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => window.addEventListener('mousemove', update))
|
||
|
|
onUnmounted(() => window.removeEventListener('mousemove', update))
|
||
|
|
|
||
|
|
return { x, y }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- Usage - simpler, no extra component -->
|
||
|
|
<script setup>
|
||
|
|
import { useMouse } from '@/composables/useMouse'
|
||
|
|
|
||
|
|
const { x, y } = useMouse()
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div>Mouse: {{ x }}, {{ y }}</div>
|
||
|
|
</template>
|
||
|
|
```
|
||
|
|
|
||
|
|
## When to Use Each Pattern
|
||
|
|
|
||
|
|
| Pattern | Best For |
|
||
|
|
|---------|----------|
|
||
|
|
| **Composables** | Pure logic reuse, state management, side effects |
|
||
|
|
| **Renderless Components** | When slot composition is needed, wrapper logic with flexible children |
|
||
|
|
|
||
|
|
### Use Composables When:
|
||
|
|
- Sharing reactive state and methods
|
||
|
|
- Encapsulating side effects (event listeners, timers)
|
||
|
|
- No need for template slot composition
|
||
|
|
- Maximum performance is needed
|
||
|
|
|
||
|
|
### Use Renderless Components When:
|
||
|
|
- Need to provide context/data to arbitrary slot content
|
||
|
|
- Building provider components (like context providers)
|
||
|
|
- The slot composition pattern is genuinely useful
|
||
|
|
- Building component libraries with maximum flexibility
|
||
|
|
|
||
|
|
## Real-World Examples
|
||
|
|
|
||
|
|
**Composable is Better:**
|
||
|
|
```typescript
|
||
|
|
// Data fetching
|
||
|
|
export function useFetch<T>(url: string) {
|
||
|
|
const data = ref<T | null>(null)
|
||
|
|
const error = ref<Error | null>(null)
|
||
|
|
const loading = ref(true)
|
||
|
|
// ... fetch logic
|
||
|
|
return { data, error, loading }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Form validation
|
||
|
|
export function useForm(initialValues) {
|
||
|
|
const values = ref(initialValues)
|
||
|
|
const errors = ref({})
|
||
|
|
const validate = () => { /* ... */ }
|
||
|
|
return { values, errors, validate }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Renderless Component is Better:**
|
||
|
|
```vue
|
||
|
|
<!-- Virtualized list that needs to wrap arbitrary item rendering -->
|
||
|
|
<VirtualScroller :items="items" :item-height="50">
|
||
|
|
<template #default="{ item, style }">
|
||
|
|
<!-- Consumer controls how each item renders -->
|
||
|
|
<div :style="style" class="custom-item">
|
||
|
|
{{ item.name }}
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</VirtualScroller>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Performance Consideration
|
||
|
|
|
||
|
|
Renderless components still:
|
||
|
|
- Create a component instance
|
||
|
|
- Go through the Vue component lifecycle
|
||
|
|
- Have reactive overhead
|
||
|
|
|
||
|
|
Composables are just functions:
|
||
|
|
- No component instance overhead
|
||
|
|
- Direct reactive primitives
|
||
|
|
- Smaller bundle size
|
||
|
|
|
||
|
|
## Reference
|
||
|
|
- [Vue.js Slots - Renderless Components](https://vuejs.org/guide/components/slots.html#renderless-components)
|
||
|
|
- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)
|