Using JSX with Vue and Why You Should Care

Vue.js has an easy API and several options for defining HTML templates in our components.

We can use the <template> tag option, define a template property on our root component instance, or use Single-File components.

The options above are awesome and work perfectly, but, there comes a time in the lifecycle of your application where they either feel clunky, over-engineered or very inflexible.

Table of Contents

    So, why would we want to JSX instead of any of the other template definitions?

    • JSX is easy to read. <div>...</div> is subjectively better than this.$createElement('div', {}, [...])
    • Seriously, It's just JavaScript.
    • Vue has support for JSX.
    • JSX makes custom Vue components easier to import and manage.

    A quick intro

    Let me give you an example of why JSX is good.

    We want to build a <TextField/> component that can either be a normal single-line text input or a multiline input (textarea). Our template declaration might look like this.

     <div>
       <textarea v-if="multiline" v-model="content" :name="name" :placeholder="placeholder" :aria-invalid="false">
       <input v-else v-model="content" :name="name" :placeholder="placeholder" :aria-invalid="false">
     </div>

    As you can see from the snippet above, we'll quickly run into a few problems like duplicate code and many more. Imagine having to support a variety of properties on the input. This little snippet above will grow and be a nightmare to maintain.

    To fix this, we need to go low-level with Vue. We need to get closer to Vue's internal API to fix this mess.

    The render() method

    Every component we create in Vue has a render method. This is where Vue chooses to render the component. Even if we don't define this method, Vue will do it for us.

    This means that when we define HTML templates in Vue — Vue's template compiler compiles it to a createElement function that takes a couple parameters and returns the result from the render function.

    To fix the code in the previous section, we remove the template property or the template tag and define a render() method on the component. If the render method is defined on a component, Vue will ignore the template definition.

     ...
     export default {
         name: 'TextField',
         render (createElement) {
             const tag = this.multiline ? 'textarea' : 'input'
    
            return createElement(tag, {
                 class: {
                     'text-input': true,
                     'is-disabled': false
                 },
                 attrs: {
                     name: this.name,
                     placeholder: this.placeholder,
                     'aria-invalid': false
                 }
             })
         } 
     }
     ...

    The above code does a few things:

    1. The render method takes a createElement helper from Vue.
    2. We programmatically define our tag.
    3. Then we create the tag and pass its attributes, classes etc as an object. There are quite a few options we can pass to createElement.
    4. We return the newly created element for rendering.

    Note: Every template we define for a Vue component will be converted into a render method that returns a createElement function. It's because of this reason the render method will take precedence over a template definition.

    Take this example:

     <div>
       <p>Only you can stop forest fires</p>
     </div>

    The template compiler will convert the HTML above into:

     ...
     render (createElement) {
         return createElement(
           'div',
           {},
           createElement(
               'p',
               {},
               'Only you can stop forest fires'
           )
         )
     }
     ...

    Okay! now you might ask this question, "Isn't this bad for readability?" The answer is yes. Once you define a component with many levels of elements nesting or has several sibling elements — we run into a new problem. We just sacrificed readability. Like they say, "we've moved from the frying pan to fire."

    Cue JSX. This is where we'll have JSX bring back the readability we lost.

    What is JSX

    If you already know about JSX, feel free to skip to the next section where I'll show you how to use JSX in Vue.

    JSX is a term coined by Facebook's engineering team.

    JSX is an XML-like syntax extension to JavaScript without any defined semantics.

    JSX is NOT intended to be implemented by engines or browsers. Instead, we'll use transpilers like Babel to convert JSX to regular JavaScript.

     // this line below is an example of JSX
     const heading = <h1>Welcome to Scotch</h1>;

    Basically, JSX lets us use an HTML-like syntax in JavaScript.

    Unfortunately, this article assumes you already know JSX, so teaching JSX is beyond the scope of this article. I'll still point you in the right direction. JSX is very easy to grok and can be done in a couple minutes.

    Use these links to learn The basics of JSX, Learn JSX in-depth, finally, if you really want to know about the specification that is JSX, visit its official website.

    Configure Vue to use JSX

    If you use Vue-cli greater or equal to version 3.0 you are in luck as JSX is supported.

    If you are using an older version of Vue-cli that doesn't support JSX, you can add it by installing babel-preset-vue-app and add it to your .babelrc file.

    To install:

     # Using npm
     npm install --save-dev babel-preset-vue-app
    
    # Using yarn
     yarn add --dev babel-preset-vue-app

    In you .babelrc file, all you have to do is:

     {
         "presets": ["vue-app"]
     }

    There, we can now use JSX in our component's render function.

    Vue's JSX syntax gotchas

    There are few gotchas to using JSX in Vue.

    First, you can no longer use the : and @ shortcuts for binding and listening to events. They are invalid JSX syntax and your code won't compile.

    To listen for events in JSX, we need the "on" prefix. For example, use onClick for click events.

     render (createElement) {
         return (
             <button onClick={this.handleClick}></button>
         )
     }

    To modify events, use:

     render (createElement) {
         return (
             <button onClick:prevent={this.handleClick}></button>
         )
     }

    To bind a variable, instead of : use:

     render (createElement) {
         return (
             <button content={this.generatedText}></button>
         )
     }

    To set HTML string as the content of an element, instead of v-html use:

     render (createElement) {
         return (
             <button domPropsInnerHTML={htmlContent}></button>
         )
     }

    We can also spread a large object.

     render (createElement) {
         return (
             <button {...this.largeProps}></button>
         )
     }

    Using JSX in render

    Going back to our initial "TextField" component. Now that we have JSX enabled in our Vue app, we can now do this.

     render (createElement) {
         const inputAttributes = {
             class: 'input-field has-outline', // class definition
             onClick: this.handleClick // event handler
             backdrop: false // custom prop
         }
         const inputMarkup = this.multiline
             ? <textarea {...inputAttributes}></textarea>
             : <input {...inputAttributes}/>
    
        return inputMarkup
     }

    Importing Vue JSX Components

    Another benefit to using JSX in Vue is that we no longer have to register every component we need. We just import and use.

    import {Button} from '../components'
    
    export default {
         render (createElement) {
             return <Button primary={true}>Edit</Button>
         }
     }

    How to make JSX work with TypeScript

    TypeScript is used as a mechanism that adds type-checking to JavaScript. You can read more.

    To add JSX support to TypeScript all we need to do is modify our tsconfig.json.

    To enable JSX in TypeScript, first save the file as a .tsx file and modify your tsconfig.json to include:

     {
       "compilerOptions": {
         ....
         "jsx": "preserve",
       }
     }

    Setting the jsx option to "preserve" means that TypeScript should not process the JSX. Doing this lets Babel take control of everything JSX and TypeScript stick to types as it does not yet support Vue JSX. You can learn more.

    Then create a jsx.d.ts file in your project and add the TypeScript JSX declarations for Vue.

    import Vue, {VNode} from 'vue'
    
    declare global {
       namespace JSX {
         interface Element extends VNode {}
         interface ElementClass extends Vue {}
         interface ElementAttributesProperty {
           $props: {}
         }
         interface IntrinsicElements {
     [elemName: string]: any
         }
       }
     }

    Make sure that TypeScript can load the declaration file. Or, you can add autoloading for it in tsconfig.json via:

     {
       "compilerOptions": {
         ...
         "typesRoot": ["./node_modules/@types", "./types"]
       }
     }

    Conclusion

    That's it for today. Enjoy having some or all of your Vue.js templates in JSX.

    And, please no complaints about JSX breaking SOC (separation of concerns), I can't take another one of those arguments. If you prefer using the createElement function with objects by all means enjoy!!

    Let me know your thoughts and suggestions in the comments.

    Cheers!

    Samuel Oloruntoba

    35 posts

    Samuel is a multi-disciplinary web developer. Nowadays, he spends most of his time in the browser trying to understand it and squeeze as much performance as he can. He's also a proficient Vue/Laravel developer and absolutely slays at FIFA. You can reach out to him for business or Scotch related matters.