Render Functions and JSX in Vue.js Development
Basics
To build our HTML, vue recommends using templates in most cases. However, there are situations where we really need the programmatic power of JavaScript in full. It is in such cases that we use the render function, which is closer to the compiler alternative when compared with templates.
A simple example that demonstrates where the render function would be practical is when we want to generate anchored headings:
<h1>
<a name="hello-mumbai" href="#hello-world">
Hello Mumbai!
</a>
</h1>
For the HTML snippet above, we can decide that we want the component interface below:
<anchored-heading :level="1">Hello Mumbai!</anchored-heading>
When we get started with a component that only generates a heading based on the level prop, we quickly arrive at this:
HTML
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>
JS
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
The template above doesn't feel great, it is verbose and we are duplicating <slot></slot> for every heading level and we will have to do the same when we add the anchor element.
While templates will work great for most components, this is clearly not one of those components. Let's try to rewrite it with a render function:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name
this.$slots.default // array of children
)
},
props: {
level: {
type: Number,
required: true
}
}
})
This is sort of much simpler and the code is shorter, but it also requires a greater familiarity with Vue instance properties. You have to know when to pass children without a v-slot directive into a component in this case. Like the Hello Mumbai! Inside of anchored-heading, the children are stored on the component instance at $slots.default.
Noder, Trees, and the Virtual DOM
Before diving into render functions, it is important that we know a little about how browsers work. Consider this HTML for example:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
When the browser reads this code, it builds a treeof "DOM nodes" that will help it to keep track of everything, just as we might build a family tree to keep track of our extended family.
Every element and piece of text is a node. Comments are nodes as well. A node is any piece on the page. And just like in a family tree, every node can have children.
Updating these nodes efficiently can be difficult, a good thing, we never have to do it manually. Rather, we tell Vue what HTML we want on the page.
In a template:
<h1>{{ blogTitle }}</h1>
Or in a render function:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
In both cases, Vue will automatically keep the page updated, even when blogTitle changes.
The Virtual DOM
Vue accomplishes its reactive nature by building a virtual DOM to keep track of the changes it needs to make to the real DOM. Consider this line in the render function:
return createElement('h1', this.blogTitle)
createElement is not actually returning a real DOM element. it could be more accurately named createNodeDescription, because it contains information describing to Vue what kind of node it should render on the page. Including the description of any child node. This node description is called a "virtual node" and is usually abbreviated to VNode. "Virtual DOM" is what we call the entire tree of VNodes built by a tree of Vue Components.
createElement Arguments
The next thing we will have to become familiar with is how to use template features in the createElement function.
// @returns {VNode}
createElement(
// {String | Object | Function}
// component options, HTML tag name, or async
// function that resolves to one of these. Required.
'div',
// {Object}
//this is a data object corresponding to the attributes
// that you would use in a template. Optional.
{
// (please see details in the next section below)
},
// {String | Array}
// the Children VNodes, these are built using `createElement()`,
// or by using strings to get 'text VNodes'. Optional.
[
'Some text comes first.',
createElement('h1', 'A headline'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
The Data Object In-Depth
There is one thing to note: similar to how v-bind:style and v-bind:class have special treatment in templates, they also have their own top-level fields in VNode data objects. This object also allows us to bind normal HTML attributes as well as DOM properties such as innerHTML (this would replace the v-html directive):
{
// Same API as `v-bind:class`, accepting either
// a string, object, or array of strings and objects.
class: {
foo: true,
bar: false
},
// Same API as `v-bind:style`, accepting either
// a string, object, or array of objects.
style: {
color: 'red',
fontSize: '14px'
},
// Normal HTML attributes
attrs: {
id: 'foo'
},
// Component props
props: {
myProp: 'bar'
},
// DOM properties
domProps: {
innerHTML: 'baz'
},
// Event handlers are nested under `on`, though
// modifiers such as in `v-on:keyup.enter` are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},
// Custom directives. Note that the `binding`'s
// `oldValue` cannot be set, as Vue keeps track
// of it for you.
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Scoped slots in the form of
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// The name of the slot, if this component is the
// child of another component
slot: 'name-of-slot',
// Other special top-level properties
key: 'myKey',
ref: 'myRef',
// If you are applying the same ref name to multiple
// elements in the render function. This will make `$refs.myRef` become an
// array
refInFor: true
}
Complete Example
With the knowledge we now have we can complete the component we started:
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
// create kebab-case id
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^-|-$)/g, '')
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
})
Constraints
VNodes Must Be Unique
All the VNodes that are in a component tree must be unique, hence the code below is invalid:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// Yikes - duplicate VNodes!
myParagraphVNode, myParagraphVNode
])
}
In order to duplicate the same element/component many times, we can use a factory function. Example:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
Replacing Template Features with Plain JavaScript
v-if and v-for
The render functions do not provide a proprietary alternative wherever something can be accomplished in plain JavaScript. A template using v-if and v-for is an example:
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
We can rewrite this is JavaScript's if/else and map in a render function
props: ['items'],
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}
v-model
v-model does not have a direct counterpart in render functions- we have to implement the logic ourselves:
props: ['value'],
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
}
Event and Key Modifiers
For he .once,.passive and .capture event modifiers, Vue offers us prefixes that can be used with on:
Modifier(s) | Prefix |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.once.capture or .capture.once | ~! |
Example:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
For all other event and key modifiers aside the once these, the proprietary prefix is not necessary, because we can use event method in the handler.
Slots
We can access static slot contents as Arrays of VNodes from this.$slots:
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
We can access scoped slots as functions that return VNodes from this.$scopedSlots:
`props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
For us to pass scoped slots to a child component using render functions, we use the scopedSlots field in VNode data:
render: function (createElement) {
return createElement('div', [
createElement('child', {
// pass `scopedSlots` in the data object
// in the form of { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
JSX
If we're writing a lot of render functions, it could feel painful to write something like this:
createElement(
'anchored-heading', {
props: {
level: 1
}
},
[
createElement('span', 'Hello'),
' world!'
]
)
More especially when the template version is so simple in comparison:
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
This's why there's a Babel plugin to use JSX with Vue, thus getting us back to a syntax that's closer to templates:
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
Functional Components
The anchored heading component that we created earlier is relatively simple. It does not manage any state passed to it, and it also does not have lifecycle methods. It is only a function that has some props.
When mark a component as functional, this means that they are stateless (no reactive data) and instances (no this context). A functional component will look like this:
Vue.component('my-component', {
functional: true,
// the Props are optional
props: {
// ...
},
// in compensation for the lack of an instance,
// we are provided a 2nd context argument.
render: function (createElement, context) {
// ...
}
})
We pass everything a component needs through context, this is an object containing:
- props: this is an object of the provided props
- children: this is an array of the VNode children
- slots: this is a function returning a slots object
- scopedSlots: as from (2.6.0+) An object that exposes passed-in scoped slots. It also exposes normal slots as functions.
- data: this is the entire data object, that is passed to the component as the 2nd argument of createElement
- parent: this is a reference to the parent component
- listeners: from (2.3.0+) An object containing parent-registered event listeners. Which is an alias to data.on
- injections: as from (2.3.0+) if using the inject option, this contains resolved injections.
Functional components are much cheaper to render, because they are just functions.
They're equally very useful as wrapper components. For instance:
- when we need to programmatically choose one of several other components to delegate to
- when we need to manipulate children, props, or data before passing them on to a child component
An example of a smart-list component that delegates to more specific components, depending on the props passed to it is shown below:
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
Passing Attributes and Events to Child Elements/Components
For normal components, attributes that are not defined as props are automatically added to the root element of the component, it replaces or intelligently merges with existing attributes of the same name.
Functional components will require us to explicitly define this behavior:
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// Transparently pass any attributes, event listeners, children, etc.
return createElement('button', context.data, context.children)
}
})
When we pass context.data as the second argument to createElement, we effectively pass down any attributes or event listeners used on my-functional-button.
If we are using template-based functional components, we will also have to manually add attributes and listeners. Since we already have access to the individual context contents, we can then use data.attrs to pass along any HTML attributes and listeners (the alias for data.on) to pass along any event listeners.
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
slots() vs children
One may wonder why we need both slots() and children. Will slots().default not be the same as children? For some cases, yes - but what if we have a functional component with the following children?
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>
In this component, children will give us both paragraphs, slots().default will give us only the second, and slots().foo will give us only the first. Having both children and slots() therefore allows us to choose whether this component knows about a slot system or perhaps delegates that responsibility to another component by passing along children.
Template Compilation
We may be interested to know that Vue's templates actually compile to render functions. This is an implementation detail we usually don't need to know about, but if we would like to see how specific template features are compiled, we may find it interesting.
Previous:
Utilizing Mixins in Vue.js for Reusable Functionality.
Next:
Understanding and Using Plugins in Vue.js Development.
It will be nice if you may share this link in any developer community or anywhere else, from where other developers may find this content. Thanks.
https://www.w3resource.com/vue/render-functions-and-jsx.php
- Weekly Trends and Language Statistics
- Weekly Trends and Language Statistics