In a Vue 3 render function, how do you pass default, named and scoped slots to a child component, and why must they be functions?
answer
- third argument of h()
- null before the slots object
- scoped slot takes a parameter
- child calls slots lazily
basics
~20 sPass a function for the default slot, or null props plus an object of slot functions for named ones. Scoped slots are functions with a parameter. Functions let the child call them lazily, so the child tracks their dependencies.
solid answer
~40 sChildren of a component vnode are slots. For only a default slot pass a function, `h(Card, () => 'hi')`; for named slots pass `null` props and an object, `h(Card, null, { default: () => ..., footer: () => ... })`. The `null` matters because with two arguments `h()` reads a plain object as props. A scoped slot is a slot function with a parameter, called by the child as `slots.item({ text })`. Slots are functions so the child invokes them lazily during its own render, which makes the child, not the parent, track the reactive state the slot content reads. Passing a vnode array still works, but Vue warns in development: 'Non-function value encountered for default slot. Prefer function slots for better performance.'
code
ts · 18 linesimport { defineComponent, h, ref } from 'vue'
const MyList = defineComponent({
setup(props, { slots }) {
const items = ref(['a', 'b'])
// the child calls the scoped slot with data for each item
return () => h('ul', items.value.map((text) => slots.item?.({ text })))
}
})
export default defineComponent({
setup() {
return () =>
h(MyList, null, {
item: ({ text }: { text: string }) => h('li', { key: text }, text)
})
}
})go deeper
Recall that slots in render functions are functions, passed as the third argument of h() after null props.
Explain the null requirement, how scoped slots receive data, and why lazy slot functions move dependency tracking into the child.
Review render-function libraries for eager slot arrays and missing null arguments, and explain the update cost eager slots create.
Set conventions for slot-heavy component APIs in render-function libraries so consumers get predictable, fine-grained updates.
## Slots are functions on both sides In a Vue 3 render function, **slots are functions**, whichever side of the parent-child boundary you are on: - the **child** receives them on the `slots` object from the setup context (`setup(props, { slots })`), and each slot is a function that returns an array of vnodes; - the **parent** passes them as the third argument to `h()`, either as a single function (the default slot) or as an object of named slot functions. This mirrors what the template compiler produces for `<template #footer>` and `v-slot` syntax, so a hand-written render function speaks the same protocol. ## Passing slots from the parent ```ts // only a default slot: a single function h(MyCard, () => 'hello') // named slots: an object of functions, props slot set to null h(MyCard, null, { default: () => 'body text', footer: () => h('small', 'footer') }) // scoped slot: the function receives the child's slot props h(MyList, null, { item: ({ text }: { text: string }) => h('li', text) }) ``` Three details interviewers probe: 1. **The `null` is required** when you pass a slots object. With two arguments, `h()` treats a plain object as props, so `h(MyCard, { default: () => 'Hi' })` hands MyCard a prop named `default` and no slot at all. 2. A slot function may return anything a render function can return, such as a string, a vnode or an array. The child always receives it **normalized to an array of vnodes**. 3. A **scoped slot** is simply a slot function with a parameter. The child calls it with data, as in `slots.item({ text })`, and the parent's function turns that data into vnodes. ## Calling slots in the child ```ts setup(props, { slots }) { const text = ref('hi') return () => h('div', slots.default?.({ text: text.value })) } ``` The child decides *when* and *how often* to call each slot, and can pass data into it. Call slots inside the render function so each render sees current content, and use `?.()` for optional slots. ## Why functions and not vnode arrays Because slots are functions, the **child invokes them lazily, during its own render**. Any reactive state the slot content reads is then tracked by the child's render effect rather than the parent's. The Vue docs describe this as leading to more accurate and efficient updates: a change used only inside slot content re-renders the child that displays it, not necessarily the parent that authored it. If you pass a plain array or vnode as a component's children instead, Vue still normalizes it into a slot, but in development it warns: > Non-function value encountered for default slot. Prefer function slots for better performance. Named slots given non-function values get the same warning, naming the slot key. | Children passed to `h(Component, ...)` | Result | |---|---| | `() => ...` | default slot | | `null, { name: () => ... }` | named or scoped slots | | `{ name: () => ... }` as the 2nd argument | read as **props**, no slots | | an array or vnode | works, but dev warning about non-function slots | ## Forwarding slots through a wrapper A wrapper component often passes some of its own slots to the component it wraps. Because both sides use the same function protocol, forwarding is just handing the functions on: `h(Inner, null, { default: slots.default, footer: slots.footer })`. The inner component calls them when it renders, and the wrapper never has to call them itself. You can also wrap one to add markup: `default: () => [h('span', 'Note: '), slots.default?.()]`. ## The JSX shape In Vue JSX the same protocol applies: children of a component are a function or an object of functions, written as `<MyCard>{{ default: () => 'body', footer: () => <small>footer</small> }}</MyCard>`. ## Common mistakes - Omitting `null` before the slots object, so the slots become props. - Passing pre-built vnode arrays, which gives up lazy evaluation and triggers the warning. - Calling `slots.default()` without `?.` when the slot is optional.
- What happens with h(MyCard, { default: () => 'Hi' })?With two arguments, `h()` treats a plain object that is not a vnode as props, so MyCard receives a prop or attribute named `default` and no slot. Pass `null` as the second argument, `h(MyCard, null, { default: () => 'Hi' })`, or pass the single function as the second argument when only the default slot is needed.
- How does the child receive slot content that was returned as a string?Slot functions can return anything a render function returns, but when the child calls the slot it always gets an array of vnodes: Vue normalizes the result, turning a string into a text vnode. That is why the child can place `slots.default?.()` directly as an element's children.
Passing a slot function is like handing the child a recipe instead of a cooked meal: the child cooks it when it serves the dish, with whatever ingredients are fresh at that moment.
saying these in an interview costs you the question
- Passes the slots object as the second argument of h()
- Passes an array of vnodes as a component's children
- Thinks only scoped slots need to be functions
- Believes slot content's dependencies are always tracked by the parent
- Expects a string-returning slot to reach the child as a string