JSX vs HTM (Hyperscript Tagged Markup)

Published on in JavaScript and React

HTM provides a transpiler-free alternative to JSX via tagged templates. HTM has some limitations, but can be used in browsers and can be good for smaller projects.

Table of contents

JSX

JSX is a syntax extension to JavaScript. It's transpiled away before it's served to the browser.

For example:

// JSX:
return <div className="foo">bar</div>

// Output:
return React.createElement('div', { className: 'foo' }, 'bar')

JSX is the default choice in like 99% of React projects. It's not a pro or a con per se, but it raises the question: why wouldn't I just use JSX instead of some alternative?

JSX pros

Great editor support

  • Syntax highlighting.
  • IntelliSense, with or without TypeScript.

Syntax errors are actual errors

The transpiler won't let syntax errors pass. This is better than HTM failing silently on errors.

ESLint plugins

Prettier support

Prettier works better with JSX than with HTM, as we'll see below.

0 bytes and no runtime overhead

JSX is transpiled away.

Shorthand syntax for fragments

I.e. <>...</>.

JSX cons

Needs a transpiler

More tooling = more things to setup and maintain.

Babel can be run in the browser, but that would be slow.

New syntax to learn

Personally I have no problems with the JSX syntax, but I have seen some people struggling with it: when to use curly braces etc.

HTM

HTM (Hyperscript Tagged Markup) is a JS library that provides a tagged template for writing JSX-like markup.

For example:

import { html } from 'htm/react'

return html`
  <div className="foo">bar</div>
`

HTM pros

No transpiler needed

Implications:

  • Can be run in the browser.
  • Less tooling to setup and maintain.

HTM is tiny and fast

Thus it's OK to use HTM in the browser, at least during development.

0 bytes and no runtime overhead if compiled away

For production builds, consider compiling HTM away using babel-plugin-htm.

No new syntax to learn

HTM is just standard tagged templates.

Syntax highlighting support in editors

See HTM's readme for an up-to-date list of editor extensions/plugins

Prettier support

Make sure to set Prettier's htmlWhitespaceSensitivity option to 'ignore' because HTM strips (most) extra whitespace away like JSX does. (There might be some differences between how HTM and JSX handle whitespace; pay attention.)

Shorthand syntax for components' closing tags

E.g. <${Footer}>content<//>.

Using the shorthand much can make things hard to read, but it might anyway be better to split large components into smaller pieces.

Supports HTML-like comments

E.g. <!-- Hey there -->.

HTML-like comments are stripped out, which is a good or a bad thing depending on the situation.

Implicit fragments

Returning html`<div /><div />` automatically wraps the two divs inside a fragment. Or actually an array: HTM issue #175 on GitHub is about implicit fragments.

HTM cons

Fails silently on syntax errors

This can cause some head scratching. Related issues on GitHub:

No IntelliSense support

No TypeScript support either. (HTM issue #73 on GitHub is about TypeScript support.) On the other hand, if you use TypeScript, why not just compile TSX away?

No ESLint plugins

Prettier quirk: <pre> tags are wonky

Inside <pre> tags, some non-significant whitespace is treated as significant, and indentation might get wonky. For example (I have borked the indentation quite much for illustrative purposes):

return html`
  <pre>
 <code   className="foo"    dangerouslySetInnerHTML=${highlightedCode
    ? { __html: highlightedCode }
    : undefined}
>
      ${!highlightedCode || plainCode}

    </code  >
     </pre>
`

Workaround: use <${'pre'}> instead! Example:

return html`
  <${'pre'}>
    <code
      className="foo"
      dangerouslySetInnerHTML=${highlightedCode
        ? { __html: highlightedCode }
        : undefined}
    >
      ${!highlightedCode || plainCode}
    </code>
  <//>
`

JSX for comparison (better formatting in my opinion):

return (
  <pre>
    <code
      className="foo"
      dangerouslySetInnerHTML={
        highlightedCode ? { __html: highlightedCode } : undefined
      }
    >
      {!highlightedCode || plainCode}
    </code>
  </pre>
)

Prettier quirk: consecutive ${expressions} can be messy

Example:

return html`
  <div>
    ${foo &&
    html`
      <p>Foo...</p>
    `} ${bar &&
    html`
      <p>Bar...</p>
    `}
  </div>
`

Workaround: put an empty comment between the two expressions. Example:

return html`
  <div>
    ${
      foo &&
      html`
        <p>Foo...</p>
      `
    }
    <!-->
    ${
      bar &&
      html`
        <p>Bar...</p>
      `
    }
  </div>
`

JSX for comparison (much better formatting):

return (
  <div>
    {foo && <p>Foo...</p>}
    {bar && <p>Bar...</p>}
  </div>
)

Having to repeat html`...` is tedious

E.g. when mapping arrays or with conditional rendering. Contrived example (notice I had to use the empty comment "hack"):

return html`
  <ul>
    ${
      items.length === 0 &&
      html`
        <li>No items</li>
      `
    }
    <!-->
    ${
      items.length > 0 &&
      items.map(
        (item) => html`
          <li key=${item.id}>${item.text}</li>
        `
      )
    }
  </ul>
`

JSX for comparison:

return (
  <ul>
    {items.length === 0 && <li>No items</li>}
    {items.length > 0 &&
      items.map((item) => <li key={item.id}>{item.text}</li>)}
  </ul>
)

HTML entities are escaped

For example, the text inside html`<span>1&ndash;2</span>` gets rendered as 1&ndash;2 instead of 1–2. (&ndash; = en dash.)

HTM issue #76 on GitHub is about how HTML entities are handled.

This makes it more difficult to use special characters in components' text contents. You could type them out as-is, but having special characters in source code is bad because it can be difficult to distinguish between similar characters. For example between - (hyphen), (en dash) and (em dash).

A workaround is to create e.g. char.js and use it like this:

// char.js
export default {
  ndash: '–',
  mdash: '—',
  // ...
}

// Component.js
import char from './char.js'
export default () => html`
  <span>1${char.ndash}2</span>
`

No support for comments inside opening/closing tags

HTM supports only HTML-like comments, so you can't comment specific props or attributes.

In JSX you can comment props and attributes:

return (
  <div
    // Make focusable for some obscure reason
    tabindex="0"
  >
    ...
  </div>
)

Can't use backticks in comments

Or they can be used but have to be escaped.

Backticks would be useful when referring to variables in comments. Escaped backticks and single quotes look silly:

return html`
  <div>
    <!-- 'foo' is something -->
    ${foo}

    <!-- But \`bar\` is something else -->
    ${bar}
  </div>
`

Verdict

I like the idea of HTM a lot: it's just JavaScript tagged templates, so there's no need for a transpiler and no need to learn new syntax.

HTM can be a good choice for browser environments (when you can't or don't want to use a transpiler) and for smaller projects.

The limitations of HTM (no IntelliSense, no ESLint plugins, etc.) can however become too limiting in bigger projects. Or at least I would imagine so, I haven't used HTM in any large projects.

Here's my non-prescriptive rules for choosing between JSX and HTM:

  • Choose HTM if:
    1. You can't or don't want to use a transpiler.
    2. You can live without IntelliSense and ESLint plugins.
    3. Optional: you struggle with JSX syntax. (In comparison, HTM is just JavaScript.)
  • Otherwise choose JSX. It requires a transpiler but is really robust.

But hey, what about another JSX alternative, hyperscript...? I'll leave the decision to you. 😁