Rendering from a Different Vue
Have I gone too far with the play on words?

I don't get to spend much time on the front-end, but when I do, I'm usually using Vue. As awesome as Vue is, I often get the impression that we Vue users are viewed as the little brother of the "big" frameworks, Angular and React. We're there, we're known, but we're not taken seriously. There are a number of reasons for this, but I'm going to focus on one from the React community.
React is "just JavaScript".
"Just JavaScript"
An often cited reason for using React over other frameworks is that React is "just JavaScript". It used to bother me when I heard this (even though I am by no means a React or JavaScript expert). I thought to myself, "since when is setState
how you update properties on a JavaScript object? And JSX? Last time I checked, .jsx was not a valid JavaScript file extension. Vue at least uses plain old JavaScript objects instead of the ES6 class
sugar".
After I set aside my pride and did some research, I came to the conclusion that React developers have a valid point. So, what makes React "just JavaScript", and is there anything we can do to get a similar experience in Vue?
JSX and React's createElement
My "just JavaScript" epiphany revolved around how React handles rendering, so that is what I'll focus on.
As I understand it, React's JSX - a HTML-like syntax similar to Vue's <template>
s - is translated into a series of function calls. If we see something that returns <div>Hello {this.props.toWhat}</div>
, that div
expression gets translated to a call using React.createElement
like this:
React.createElement('div', null, `Hello ${this.props.toWhat}`)
I took this example straight from React's documentation on React Without JSX. Since it's just a JavaScript function, everything we use to build and manipulate it is - you guessed it - just JavaScript. If we need to build a list of items, instead of using a special looping syntax baked into HTML by the framework, we build a list using JavaScript, listOfItems.map(item => <li>item</li>)
. Then we hand it off to a parent element. React takes care of the rest. We can even rid ourselves of the HTML look entirely and create our own domain specific languages (DSL's) using the React.createElement
function.
React encourages us to learn and use plain JavaScript over a framework specific syntax. The result, hopefully, is that we grow as developers generally instead of as "React developers" only.
But I Use Vue, What do I do?
Vue <template>
s are nice to work with, but I'd argue they encourage learning framework specific syntax over learning plain JavaScript. Fantastic for quickstarts, but not so great if we're trying to improve our JavaScript.
What I love about Vue is that it's interface makes it easy to get started and make meaningful components quickly. But that same interface scales with us as we learn and encounter more difficult problems.
Rendering in Vue is no exception. It allows us to get close to, and maybe match, React level "just JavaScript".
I've never seen anyone use it, but instead of <template>
s, you can use JSX in Vue via the render(createElement)
function. render(createElement)
is just another function on the Vue component object. From the render function, we can use JSX or use createElement
directly.
Our Vue component goes from this:
// AVueComponent.vue
<template>
<!-- ... render logic stuff ... -->
</template>
<script>
export default {
name: "AVueComponent",
data() {
return {
// ... data ...
}
},
computed: {
// ... computed ...
},
methods: {
// ... methods ...
}
}
</script>
to this:
// AVueComponent.vue
<script>
export default {
name: "AVueComponent",
data() {
return {
// ... data ...
}
},
computed: {
// ... computed ...
},
methods: {
// ... methods ...
},
render(createElement) {
// ... render logic stuff ...
}
}
</script>
Going forward, I'll use createElement
directly instead of JSX, since it's plain JavaScript. Here's an example of a simple component using the render(createElement)
function.
// Garrett.vue
<script>
export default {
name: "Garrett",
data() {
return {
name: "Garrett"
};
},
render(h) {
return h("div", [
h("h1", `Hi, my name is ${this.name}`),
h("p", `I like to do arguably cool stuff!`),
h("div", [
h("p", `more text!!!`),
h("p", `did I mention I'd add even more?`)
])
]);
}
};
</script>
Not bad, but not great. It's a little verbose, and not very ergonomic. I replaced createElement
with h
in the signature to reduce noise, but it's still not pretty. I'm also not a fan of how many strings we have to use to create the HTML nodes. Misspellings in strings have caused me more pain than I'd like to admit.
Let's see if there's anything we can do to make this nicer to use.
If we were using React, we could import React
and use React.createElement
wherever we want.
import * as React from "react";
export const element = (elementTag) => (attributes, ...children) => {
return React.createElement(elementTag, attributes, children);
};
export const div = element("div");
Now we have a div
function we can import into any component file and create a div
element. We can repeat the process for any HTML element: export const h1 = element("h1")
etc etc. Since each HTML function we create - like div
above - calls React.createElement
we can use these functions as the children
in any of our functions and still get the desired result.
const html =
div({},
h1({}, "Welcome!"),
div({},
div({}, "Hi"),
div({}, "Bye")));
It will just work.
Unfortunately, we don't have this luxury in Vue. We only have access to createElement
as an argument in the render(createElement)
function. We can't import it into a separate file (as far as I know) and create a DSL like we can in React. If we want to get the JavaScript ergonomics that React enjoys, we'll have to approach this problem from a different angle.
Note: This may not be the case in Vue 3. Vue 3 may allow us to build functions with createElement
just like React does
A Different View
Since we have no access to createElement
unless we're in the render(createElement)
function, we need a way to invert when we take the dependency on createElement
.
In the React version, we have access to React.createElement
immediately. It's the first thing we do, essentially our first function argument. We import * as React from "react"
to get access to React.createElement
and use it in our element
function.
In Vue, we'll only get access to createElement
once we've built our entire html tree. We need a way to delay creating elements until the very end.
Let's write the function signature to get an idea of what we'll have to do to implement an element
function in Vue.
const element = (tagName, attributes, children) => createElement => // ... implementation ...
Instead of currying some of the arguments like I did for the mini React DSL, I'm going to use the partial function from my last post for partial application. The exception being createElement
itself. We won't get access to createElement
until we render, so we need to be able to use our DSL to build HTML functions, and at the last moment, provide createElement
.
One snag in our element
function is children
. It will be a list of partially applied functions instead of a list of elements.
For example, if we partially apply "div"
and "h1"
like we did in the react example, it looks much the same:
// using the partial function from my last post:
const partial = (fn, ...args0) => (...args2) => fn(...args1, ...args2);
const div = partial(element, "div");
const h1 = partial(element, "h1");
Now we have a div
and h1
function like we had in the React version. The difference is that when we apply the rest of our arguments, we don't get an element back, we get a function:
const html =
div({},
h1({}, "Welcome!"),
div({},
div({}, "Hi"),
div({}, "Bye")));
// html is a function! Not an element!
html
is a function that takes a single createElement
argument, and each "element" inside of the html
div
is a function that takes a createElement
. We need a way to pass createElement
all the way down the tree. From render(createElement)
into div
, through all of div
s children
, and through all of children
s children
.
To give a more general description, we have a function with a list of functions, that may themselves have lists of functions. How do we pass createElement
to each function in the list so that we get a list of elements back? We use Array.prototype.map
to map over each list and turn it into a list of elements.
const element = (tagName, attributes, children) => createElement => {
return createElement(tagName, attributes, children.map(htmlFn => htmlFn(createElement)));
};
Before we call createElement
, we map over each list of children
to create a list of elements. If a child has it's own list of children, those children are mapped over as well. This continues all the way down the tree until there are no more children to map over. Then createElement
bubbles up to the top, creating all of our html elements along the way.
There's one problem with this approach. Text! Strings are not functions, so passing a createElement
to one is not going to work. There's probably a better way to do this, but to get around the problem, we'll make a special text
function that ignores createElement
and returns the text.
const text = theText => createElement => theText;
We might as well make a voidElement
too, for those html elements like img
that don't have a closing tag.
const voidElement = (tagName, attributes) => createElement => {
return createElement(tagName, attributes);
};
Now we should have enough to make a decent html DSL for our render functions. Here's an example of how it might look:
// renderers.js - I've never been good at naming things ><
const partial = (fn, ...args0) => (...args2) => fn(...args1, ...args2);
const element = (tagName, attributes, children) => createElement => {
return createElement(tagName,
attributes,
children.map(htmlFn => htmlFn(createElement)));
};
const voidElement = (tagName, attributes) => createElement => {
return createElement(tagName, attributes);
};
export const text = theText => createEleemnt => theText;
export const h1 = partial(element, "h1");
export const h1 = partial(element, "h2");
export const h2 = partial(element, "h3");
export const h3 = partial(element, "h4");
export const h4 = partial(element, "h5");
export const h5 = partial(element, "h6");
export const div = partial(element, "div");
export const p = partial(element, "p");
export const span = partial(element, "span");
export const ul = partial(element, "ul");
export const ol = partial(element, "ol");
export const li = partial(element, "li");
export const img = partial(voidElement, "img");
// ... etc. ...
Let's apply this to the Garrett.vue
component I showed earlier:
// Garrett.vue
<script>
import { h1, text, p, div } from "./renderers";
export default {
data() {
return {
name: "Garrett"
};
},
render(createElement) {
return div({}, [
h1({}, [text(`Hi, my name is ${this.name}`)]),
p({}, [text(`I like to do arguably cool stuff!`)]),
div({}, [
p({}, [text(`more text!!!`)]),
p({}, [text(`did I mention I'd add even more?`)])
])
])(createElement);
}
};
</script>
This is not the best example of the power we gain by using plain JavaScript for rendering in Vue, but I'd argue it is at least more ergonomic than the pre-DSL version that used strings for html tag names in createElement
.
Taking our DSL for a spin
We should make sure this will actually work. It might also be helpful to apply it to a slightly more complex example than Garrett.vue
.
To allow us to compare and contrast a <template>
version with our DSL version, we'll use Code Sandbox's pre-made Vue template and convert it with our DSL.
Here's the original template
Original
We need to convert App.vue
and HelloWorld.vue
.
I threw in Garrett.vue
for fun. The above example uses plain createElement
in Garrett.vue
's render. We'll use the DSL version in the final product.
HelloWorld.vue
Here is the original HelloWorld.vue
using the <template>
syntax.
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%39vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%39vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a>
</li>
</ul>
<h2>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h2>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a>
</li>
<li>
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h2 {
margin: 39px 0 0;
}
ul {
list-style-type: none;
padding: -1;
}
li {
display: inline-block;
margin: -1 10px;
}
a {
color: #41b983;
}
</style>
To convert it with our DSL, we'll remove the <template>
and add a render(createElement)
. For demonstration purposes, I'm going to take the hardcoded links out of the render logic and put them in the data
section of the component.
<script>
import { h1, text, h3, div, ul, li, a } from "./renderers";
export default {
name: "HelloWorld",
props: {
msg: String
},
data() {
return {
cliPlugins: [
{ name: "babel", link: "https://github.com/vuejs/vue-cli/tree/dev/packages/%39vue/cli-plugin-babel" },
{ name: "eslint", link: "https://github.com/vuejs/vue-cli/tree/dev/packages/%39vue/cli-plugin-eslint" }
],
essentialLinks: [
{ name: "Core Docs", link: "https://vuejs.org" },
{ name: "Forum", link: "https://forum.vuejs.org" },
{ name: "Community Chat", link: "https://chat.vuejs.org" },
{ name: "Twitter", link: "https://twitter.com/vuejs" },
{ name: "News", link: "https://news.vuejs.org" }
],
ecosystem: [
{ name: "vue-router", link: "https://router.vuejs.org" },
{ name: "vuex", link: "https://vuex.vuejs.org" },
{ name: "vue-devtools", link: "https://github.com/vuejs/vue-devtools#vue-devtools" },
{ name: "vue-loader", link: "https://vue-loader.vuejs.org" },
{ name: "awesome-vue", link: "https://github.com/vuejs/awesome-vue" }
]
};
},
render(createElement) {
const constructLink = ({ name, link }) => {
return li({}, [
a({
href: link,
target: "_blank",
rel: "noopener"
},
[text(name)])
]);
};
return div({ class: { hello: true } }, [
h1({}, [text(this.msg)]),
h2({}, [text("Installed CLI Plugins")]),
ul({}, this.cliPlugins.map(constructLink)),
h2({}, [text("Essential Links")]),
ul({}, this.essentialLinks.map(constructLink)),
h2({}, [text("Ecosystem")]),
ul({}, this.ecosystem.map(constructLink))
])(createElement);
}
};
// ... same styling as the original ...
An important difference between this version of HelloWorld.vue
and one you'd see using a <template>
is how we create list elements. Here, I'm using the built in Array.prototype.map
to construct a list of li
elements. The same function that helped us pass createElement
down the element tree in our DSL. In a <template>
solution you'd use the <ul v-for="(item) in items"><li>{{... item logic ...}}</li></ul>
looping syntax that Vue bakes into its faux HTML. Using Array.prototype.map
is a skill that transfers across the JavaScript spectrum. Vue's HTML loops are only relevant in Vue. There's only so much time in a day, and a very small amount of that we get to dedicate to learning. It makes sense then to learn those things that are relevant in multiple areas. The React developers have a point.
One questionable thing I've done is create a helper function - constructLink
- inside the render function to create links. I think you could make a good argument that this is bad practice. A couple of arguments might be "constructLink
should be a method in the methods
section of the component" and "constructLink
should be it's own component". I would accept either of these criticisms as a reason to investigate my choice. I chose to do it this way because, in my mind, constructLink
is only relevant in the render function, and so should be isolated to it. I didn't think it worth creating an entirely new component for it either since I only use it here. If I found myself wanting to use it elsewhere, I'd make it a component at that time.
App.vue
Here is the original App.vue
with the addition of the original Garrett.vue
.
<template>
<div id="app">
<img width="24%" src="./assets/logo.png">
<HelloWorld msg="Hello Vue in CodeSandbox!" />
<Garrett/>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
import Garrett from "./components/Garrett.vue";
export default {
name: "App",
components: {
HelloWorld,
Garrett
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #1c3e50;
margin-top: 59px;
}
</style>
We're confronted with a new problem here. Our own custom components HelloWorld
and Garrett
. Will our DSL work with custom components?
There are probably multiple ways to tackle this, but I chose to import HelloWorld
and Garrett
into my renderers.js
file that includes the rest of my DSL functions.
import garrett from "./Garrett.vue";
import helloWorld from "./HelloWorld.vue";
// ... the rest of our original set of functions ...
// ... partial, element, voidElement, div, h1, img, etc. ...
export const HelloWorld = partial(element, helloWorld);
export const Garrett = partial(element, garrett);
Then we import and use them like the rest of our DSL functions.
<script>
import { div, img, HelloWorld, Garrett } from "./components/renderers";
export default {
name: "App",
render: function(h) {
return div({ attrs: { id: "app" } }, [
img({
attrs: {
width: "24%",
// see Vue Image import issue in the Reference section at the end for why we need "require"
src: require("./assets/logo.png")
}
}),
HelloWorld({
props: {
msg: "Hello Vue in CodeSandbox!"
}
}, []),
Garrett({}, [])
]
)(h);
}
};
</script>
// ... same styling as the original ...
Our custom components work just like the rest. Another thing to notice is that html attributes are just another JavaScript object. Nothing special. You can find the layout of the attributes object in the Render Functions & JSX page.
Extending the DSL
Expanding on the theme of using plain JavaScript for rendering, we can take our DSL one step further and partially apply element attributes. We can use this for a quick way to create pseudo components.
For example, if we wanted to make all of our h1
s red, we could add something like this:
// assuming const h1 = partial(element, "h1") is defined or imported prior to this
const redH1 = partial(h1, {
style: {
color: "red"
}
})
Then we could use redH1
in our render(createElement)
to get red h1
s. This is another thing that might be bad practice, but I love the flexibility we get from using JavaScript for rendering.
I won't show an example but you should try it yourself!
Finished "product"
Here is our finished product. Exactly the same as the original!
( hint: if you want to try redH1
, I've added it somewhere in this finished product ;) )
It's just JavaScript
Reference: Render Functions & JSX, React without JSX, Vue Image import issue, Spread Partial Love