JSX vs HTM (Hyperscript Tagged Markup)

Published on in JavaScript and React

Last updated on

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

Great 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

JSX is not regular JavaScript; it's a syntax extension to JavaScript.

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. Probably in production as well (just get started and optimize performance later if needed!).

0 bytes and no runtime overhead if compiled away

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

No new JavaScript syntax to learn

HTM is just standard tagged templates, i.e. regular JavaScript (as opposed to JSX, which is a syntax extension to JavaScript).

HTM needs to be used in a certain way, so you do need to learn "HTM syntax," but no new JavaScript syntax.

Syntax highlighting support in editors

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

OK Prettier support

Prettier supports HTM, and that's great. I wouldn't personally even consider using HTM if Prettier didn't support it.

But the outcome is not always very pretty, as we'll see below.

I recommend setting 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 or TypeScript support

HTM issue #73 on GitHub is about TypeScript support.

On the other hand, if you use TypeScript, why not just use TSX? It's compiled away.

No ESLint plugins

Prettier quirk: <pre> tags are wonky

As mentioned above, HTM strips (most) extra whitespace away like JSX does.

However, inside <pre> tags Prettier treats some non-significant whitespace as significant, so the indentation can get really wonky.

Example (tested with Prettier 2.2.0 and 2.6.2); I have borked the indentation quite much for illustrative purposes:

// This code is formatted with Prettier but is very ugly!
return html`
  <pre>
 <code   className="foo"    dangerouslySetInnerHTML=${highlightedCode
    ? { __html: highlightedCode }
    : undefined}
>
      ${!highlightedCode || plainCode}

    </code  >
     </pre>
`

Workaround: use <${'pre'}> instead! Then Prettier won't treat non-significant whitespace as significant. Example:

// This code is formatted with Prettier, looks OK
return html`
  <${'pre'}>
    <code
      className="foo"
      dangerouslySetInnerHTML=${highlightedCode
        ? { __html: highlightedCode }
        : undefined}
    >
      ${!highlightedCode || plainCode}
    </code>
  <//>
`

JSX for comparison (better formatting in my opinion):

// This code is formatted with Prettier, looks beautiful!
return (
  <pre>
    <code
      className="foo"
      dangerouslySetInnerHTML={
        highlightedCode ? { __html: highlightedCode } : undefined
      }
    >
      {!highlightedCode || plainCode}
    </code>
  </pre>
)

Obsolete: Prettier quirk: consecutive ${expressions} can be messy

Update: I noticed that Prettier has this quirk only when formatting JS code blocks in Markdown files, not when formatting JS files. My bad!

Example of messy consecutive ${expressions} (click to toggle)

Tested with Prettier 2.2.0 and 2.6.2; notice how the two expressions end/start on the same line (but only when formatting a JS code block in a Markdown file!):

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

Having to repeat html`...` is tedious

E.g. when mapping arrays or with conditional rendering.

I also dislike how Prettier spans the code on many more lines than with JSX.

Contrived example:

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 (much more compact):

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:

<button
  className="foo"
  disabled // Disabled because x
  id="bar"
  // Using `baz` instead of `qux` because y
  onClick={baz}
>
  Click me
</button>

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>
`

JSX for comparison:

return (
  <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 JavaScript 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 JSX is really robust.

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