--- title: Pass Slots as Functions in Render Functions, Not Direct Children impact: HIGH impactDescription: Passing slot content incorrectly causes slots to not render or be treated as props type: gotcha tags: [vue3, render-function, slots, children, vnode] --- # Pass Slots as Functions in Render Functions, Not Direct Children **Impact: HIGH** - When creating component vnodes with `h()`, children must be passed as slot functions, not as direct children. Passing children directly may cause them to be interpreted as props or fail to render. ## Task Checklist - [ ] Pass slot content as functions: `{ default: () => [...] }` - [ ] Use `null` for props when only passing slots to avoid misinterpretation - [ ] For default slot only, a single function can be passed directly - [ ] For named slots, use an object with slot function properties **Incorrect:** ```js import { h } from 'vue' import MyComponent from './MyComponent.vue' // WRONG: Children array may be misinterpreted h(MyComponent, [ h('span', 'Slot content') // May not render as expected ]) // WRONG: Named slots as direct properties h(MyComponent, { header: h('h1', 'Title'), // This is a prop, not a slot! default: h('p', 'Content') // This is also a prop }) ``` **Correct:** ```js import { h } from 'vue' import MyComponent from './MyComponent.vue' // CORRECT: Default slot as function h(MyComponent, null, { default: () => h('span', 'Slot content') }) // CORRECT: Single default slot shorthand h(MyComponent, null, () => h('span', 'Slot content')) // CORRECT: Named slots as functions h(MyComponent, null, { header: () => h('h1', 'Title'), default: () => h('p', 'Content'), footer: () => [ h('span', 'Footer item 1'), h('span', 'Footer item 2') ] }) // CORRECT: With props AND slots h(MyComponent, { size: 'large' }, { default: () => 'Button Text' }) ``` ## Why Functions? Slots in Vue 3 are functions for lazy evaluation: ```js // Slots are called by the child component when needed // This enables: // 1. Scoped slots (passing data back) // 2. Conditional rendering (slot not called if not used) // 3. Proper reactivity tracking h(MyList, { items }, { // Scoped slot - receives data from child item: ({ item, index }) => h('li', `${index}: ${item.name}`) }) ``` ## The null Props Gotcha When passing only slots, always use `null` for props: ```js // WRONG: Slots object interpreted as props! h(MyComponent, { default: () => 'Hello' }) // MyComponent receives: props.default = () => 'Hello' // CORRECT: null signals "no props, next arg is slots" h(MyComponent, null, { default: () => 'Hello' }) // MyComponent receives slot correctly ``` ## Forwarding Slots from Parent ```js export default { setup(props, { slots }) { return () => h(ChildComponent, null, { // Forward all slots from parent ...slots, // Or forward specific slots default: slots.default, header: slots.header }) } } ``` ## Scoped Slots in Render Functions ```js // Parent: Receives data from child via slot props h(DataTable, { data: items }, { row: (slotProps) => h('tr', [ h('td', slotProps.item.name), h('td', slotProps.item.value) ]) }) // Child (DataTable): Calls slot with data export default { props: ['data'], setup(props, { slots }) { return () => h('table', [ h('tbody', props.data.map(item => // Pass data to slot function slots.row?.({ item }) ) ) ]) } } ``` ## Common Patterns ```js // Wrapper component forwarding slots h('div', { class: 'wrapper' }, [ h(InnerComponent, null, slots) ]) // Conditional slot rendering h('div', [ slots.header?.(), // Optional chaining - only render if slot provided h('main', slots.default?.()), slots.footer?.() ]) // Slot with fallback content h('div', [ slots.default?.() ?? h('p', 'Default content when slot not provided') ]) ``` ## Reference - [Vue.js Render Functions - Passing Slots](https://vuejs.org/guide/extras/render-function.html#passing-slots) - [Vue.js Render Functions - Children](https://vuejs.org/guide/extras/render-function.html#children)