Skip to content

Expand the page on render functions, especially their use of slots #712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 28, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 142 additions & 10 deletions src/guide/render-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ const app = Vue.createApp({})

app.component('anchored-heading', {
render() {
const { h } = Vue

return h(
return Vue.h(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only example that was written this way, so I've changed it to match the others.

'h' + this.level, // tag name
{}, // props/attributes
this.$slots.default() // array of children
Expand Down Expand Up @@ -164,6 +162,8 @@ h(
)
```

If there are no props then the children can usually be passed as the second argument. In cases where that would be ambiguous, `null` can be passed as the second argument to keep the children as the third argument.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make this explicit so I could use it in the subsequent examples.


## Complete Example

With this knowledge, we can now finish the component we started:
Expand Down Expand Up @@ -240,6 +240,47 @@ render() {
}
```

## Creating Component VNodes

To create a VNode for a component, the first argument passed to `h` should be the component itself:

```js
render() {
return Vue.h(ButtonCounter)
}
```

If we need to resolve a component by name then we can call `resolveComponent`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When are some use cases for resolving a component by name?

You mention later that it's often unnecessary, so as a reader, I'm curious to know a scenario where knowing this helper method would be important.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the next few sentences to try to clarify. I struggled to find a wording that wasn't a clumsy mess. It still doesn't flow as cleanly as I'd like but hopefully it's clear enough.


```js
render() {
const ButtonCounter = Vue.resolveComponent('ButtonCounter')
return Vue.h(ButtonCounter)
}
```

`resolveComponent` is the same function that templates use internally to resolve components by name.

A `render` function will normally only need to use `resolveComponent` for components that are [registered globally](/guide/component-registration.html#global-registration). [Local component registration](/guide/component-registration.html#local-registration) can usually be skipped altogether. Consider the following example:

```js
// We can simplify this
components: {
ButtonCounter
},
render() {
return Vue.h(Vue.resolveComponent('ButtonCounter'))
}
```

Rather than registering a component by name and then looking it up we can use it directly instead:

```js
render() {
return Vue.h(ButtonCounter)
}
```

## Replacing Template Features with Plain JavaScript

### `v-if` and `v-for`
Expand Down Expand Up @@ -268,6 +309,8 @@ render() {
}
```

In a template it can be useful to use a `<template>` tag to hold a `v-if` or `v-for` directive. When migrating to a `render` function, the `<template>` tag is no longer required and can be discarded.

### `v-model`

The `v-model` directive is expanded to `modelValue` and `onUpdate:modelValue` props during template compilation—we will have to provide these props ourselves:
Expand Down Expand Up @@ -297,7 +340,7 @@ render() {

#### Event Modifiers

For the `.passive`, `.capture`, and `.once` event modifiers, they can be concatenated after event name using camel case.
For the `.passive`, `.capture`, and `.once` event modifiers, they can be concatenated after the event name using camel case.

For example:

Expand All @@ -306,7 +349,7 @@ render() {
return Vue.h('input', {
onClickCapture: this.doThisInCapturingMode,
onKeyupOnce: this.doThisOnce,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode
})
}
```
Expand Down Expand Up @@ -346,34 +389,34 @@ render() {

### Slots

You can access slot contents as Arrays of VNodes from [`this.$slots`](../api/instance-properties.html#slots):
We can access slot contents as Arrays of VNodes from [`this.$slots`](../api/instance-properties.html#slots):

```js
render() {
// `<div><slot></slot></div>`
return Vue.h('div', {}, this.$slots.default())
return Vue.h('div', this.$slots.default())
}
```

```js
props: ['message'],
render() {
// `<div><slot :text="message"></slot></div>`
return Vue.h('div', {}, this.$slots.default({
return Vue.h('div', this.$slots.default({
text: this.message
}))
}
```

To pass slots to a child component using render functions:
For component VNodes, we need to pass the children to `h` as an Object rather than an Array. Each property is used to populate the slot of the same name:

```js
render() {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return Vue.h('div', [
Vue.h(
Vue.resolveComponent('child'),
{},
null,
// pass `slots` as the children object
// in the form of { name: props => VNode | Array<VNode> }
{
Expand All @@ -384,6 +427,95 @@ render() {
}
```

The slots are passed as functions, allowing the child component to control the creation of each slot's contents. Any reactive data should be accessed within the slot function to ensure that it's registered as a dependency of the child component and not the parent. Conversely, calls to `resolveComponent` should be made outside the slot function, otherwise they'll resolve relative to the wrong component:

```js
// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
// Calls to resolveComponent should be outside the slot function
const Button = Vue.resolveComponent('MyButton')
const Icon = Vue.resolveComponent('MyIcon')

return Vue.h(
Button,
null,
{
// Use an arrow function to preserve the `this` value
default: (props) => {
// Reactive properties should be read inside the slot function
// so that they become dependencies of the child's rendering
return [
Vue.h(Icon, { name: this.icon }),
this.text
]
}
}
)
}
```

If a component receives slots from its parent, they can be passed on directly to a child component:

```js
render() {
return Vue.h(Panel, null, this.$slots)
}
```

They can also be passed individually or wrapped as appropriate:

```js
render() {
return Vue.h(
Panel,
null,
{
// If we want to pass on a slot function we can
header: this.$slots.header,

// If we need to manipulate the slot in some way
// then we need to wrap it in a new function
default: (props) => {
const children = this.$slots.default ? this.$slots.default(props) : []

return children.concat(Vue.h('div', 'Extra child'))
}
}
)
}
```

### `<component>` and `is`

Behind the scenes, templates use `resolveDynamicComponent` to implement the `is` attribute. We can use the same function if we need all the flexibility provided by `is` in our `render` function:

```js
// `<component :is="name"></component>`
render() {
const Component = Vue.resolveDynamicComponent(this.name)
return Vue.h(Component)
}
```

Just like `is`, `resolveDynamicComponent` supports passing a component name, an HTML element name, or a component options object.

However, that level of flexibility is usually not required. It's often possible to replace `resolveDynamicComponent` with a more direct alternative.

For example, if we only need to support component names then `resolveComponent` can be used instead.

If the VNode is always an HTML element then we can pass its name directly to `h`:

```js
// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
return Vue.h(this.bold ? 'strong' : 'em')
}
```

Similarly, if the value passed to `is` is a component options object then there's no need to resolve anything, it can be passed directly as the first argument of `h`.

Much like a `<template>` tag, a `<component>` tag is only required in templates as a syntactical placeholder and should be discarded when migrating to a `render` function.

## JSX

If we're writing a lot of `render` functions, it might feel painful to write something like this:
Expand Down