Rendering from a Different Vue

Have I gone too far with the play on words?

Rendering from a Different Vue

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 divs children, and through all of childrens 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 h1s 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 h1s. 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