While using vue, in most cases we predefine everything possible in the template. However in certain cases, more freedom in constructing content is welcome, hence the render function comes in our sight, so is the JSX. But here I want to talk about something less free - using JSX together with template. And in composition-api.

Scenario

Considering the following component:

<template>
  <div>
    <div>{greeting}</div>
    <button @click="handleGreeting">Greeing!</button>
  </div>
</template>

<script lang="ts">
import { ref } from "@vue/composition-api"

export default defineComponent({
  setup() {
    const greeting = ref("")
    const username = ref("admin") // say you get the username somewhere else
    const handleGreeting = ()=>{
      const date = (new Date()).toLocaleDateString("en-us")
      greeing.value = `hello ${username.value}, it's ${date}`
    }

    return {
      handleGreeting,
      greeting
    }
  }
})
</script>

When you click the welcome button, a message looks like Hello, it's 1/1/2023 is displayed, and it's not attracting. What if we emphasis the date a bit?

Not a solution

Let's change the template so the date gets pronounced

<template>
  <div>
    <div>hello {username}, it's <span class="big">{date}</span></div>
    <button @click="handleGreeting">Greeing!</button>
  </div>
</template>

and

import { ref } from "@vue/composition-api"

export default defineComponent({
  setup() {
    const greeting = ref("")
    const username = ref("admin") // say you get the username somewhere else
    const date = ref("")
    const handleGreeting = ()=>{
      date.value = (new Date()).toLocaleDateString("en-us")
    }

    return {
      handleGreeting,
      username,
      date 
    }
  }
})

This time, the date looks more obvious, but oops, "it's" was displayed before we click the button!

<template>
  <div>
    <div v-if="username">hello {username}, it's <span class="big">{date}</span></div>
    <button @click="handleGreeting">Greeing!</button>
  </div>
</template>

All done, fixed!

More problems

Then the username is not so significant right? We should also emphasis it, but only when it's our VIP user!

<template>
  <div>
    <div>hello <span :class="{strong:vip}">{username}</span>, it's <span class="big">{date}</span></div>
    <button @click="handleGreeting">Greeing!</button>
  </div>
</template>

right? Right and wrong.
When using a template, everything is like so determined. When a new variable is added, the template logic gets so much more complicated and this is where dynamic content construction comes to play.

using createElement

Vue provided us a way to create VNode dynamically and use it as a component. With which we can freely change our output in whatever shape we like.

<template>
  <div>
    <div>{greeting}</div>
    <button @click="handleGreeting">Greeing!</button>
  </div>
</template>

and createElement is available in composition-api as h for conventional reason.

import { ref, h } from "@vue/composition-api"

export default defineComponent({
  setup() {
    const greeting = ref<any>(null)
    const handleGreeting = ()=>{
      const vip = true // get vip dynamically
      const username = "admin"
      const date = (new Date()).toLocaleDateString("en-us")
      greeting.value = h("div", null, [
        h("span", vip?{ style: {fontSize: "14px"}}: null, username),
        "it's",
        h("span", { style: {fontWeight: "bold"}}, date)]
      )
    }

    return {
      handleGreeting,
      greeting
    }
  }
})

Hmmm, looks even more complicated and ugly. And it didn't even work!

That's because vue doesn't allow us to use vnode in template directly and JSX is what you're here for, right?

JSX

change the script to using JSX

import { ref, h } from "@vue/composition-api"

export default defineComponent({
  setup() {
    const greeting = ref<any>(null)
    const handleGreeting = ()=>{
      const vip = true // get vip dynamically
      const username = "admin"
      const date = (new Date()).toLocaleDateString("en-us")
      greeting.value = (
        <div>
          <span style={vip?{fontSize: "14px"}:{}}>{username}</span> it's <b>{date}</b>
        </div>
      )
    }

    return {
      handleGreeting,
      greeting
    }
  }
})

And, it doesn't work and that's where all magic begins.

Firstly, we need to let webpack stop optimizing the unused import "h", and more importantly, we need to change the way we code so that the h can be accessed

<script lang="tsx"> // to make jsx work, language must to set to `tsx` instead of `ts`
import { ref, h } from "@vue/composition-api"

export default defineComponent({
  setup: ()=>{ // setup must be an arrow function so that the self can be correctly pointed.
    const _ = h // trick webpack to think that the h is used.
    const greeting = ref<any>(null)
    const handleGreeting = ()=>{
      const vip = true // get vip dynamically
      const username = "admin"
      const date = (new Date()).toLocaleDateString("en-us")
      greeting.value = (
        <div>
          <span style={vip?{fontSize: "14px"}:{}}>{username}</span> it's <b>{date}</b>
        </div>
      )
    }

    return {
      handleGreeting,
      greeting
    }
  }
})
</script>

and lastly, we need a proxy component that displays our JSX

<script lang="ts">
import { defineComponent } from "@vue/composition-api"

export default defineComponent({
  props: {
    content: {
      type: Object,
    },
  },
  name: "VNode",
  render(): any {
    return this.content
  },
})
</script>

and finally change template to display content with our wrapper component and this is the finally result:

<template>
  <div>
    <VNode :content="greeting"></VNode>
    <button @click="handleGreeting">Greeing!</button>
  </div>
</template>

<script lang="tsx">
import { ref, h } from "@vue/composition-api"
import VNode from "./VNode.vue"

export default defineComponent({
  components: {VNode},
  setup: ()=>{ 
    const _ = h 
    const greeting = ref<any>(null)
    const handleGreeting = ()=>{
      const vip = true // get vip dynamically
      const username = "admin"
      const date = (new Date()).toLocaleDateString("en-us")
      greeting.value = (
        <div>
          <span style={vip?{fontSize: "14px"}:{}}>{username}</span> it's <b>{date}</b>
        </div>
      )
    }

    return {
      handleGreeting,
      greeting
    }
  }
})
</script>

Vue2.7 walkaround

Things have changed in vue2.7 which now has composition-api built-in. However, the h function comes with vue2.7 doesn't allow you to call it out of the setup function. But that's because it needs to access the Vue instance which is only possible during setup.

Here is a fix:

Write a "hook" to retrieve the instance and save it for later use.

const useH = function () {
  let _a;
  const instance =
    (this === null || this === void 0 ? void 0 : this.proxy) ||
    ((_a = getCurrentInstance()) === null || _a === void 0 ? void 0 : _a.proxy);
  if (!instance) {
    console.error("useH must be called in setup function.");
    return
  }
  return function () {
    return instance.$createElement.apply(instance, arguments);
  };
};

Then change the script:


<script lang="tsx">
import { ref } from "vue" // now all composition-apis are exported directly from vue
import { useH } from "./hooks"
import VNode from "./VNode.vue"

export default defineComponent({
  components: {VNode},
  setup: ()=>{ 
    const h = useH() // important, the hook must be called in the setup and the variable must be named "h"
    const greeting = ref<any>(null)
    const handleGreeting = ()=>{
      const vip = true // get vip dynamically
      const username = "admin"
      const date = (new Date()).toLocaleDateString("en-us")
      greeting.value = (
        <div>
          <span style={vip?{fontSize: "14px"}:{}}>{username}</span> it's <b>{date}</b>
        </div>
      )
    }

    return {
      handleGreeting,
      greeting
    }
  }
})
</script>

By defining our h function, our JSX should now work properly.