Hyperscript: hyperior alternative to JSX
Published on in JavaScript, Mithril.js and React
Last updated on
JSX is just JS under the hood.
Hyperscript is like that JS,
but better.
Example:
h('a.link', { href }, 'Click me')
is nicer than
<a className="link" href={href}>Click me</a>
,
right? Right?!
Table of contents
How JSX works
JSX is not valid JS – it's a syntax extension to JS. Let's consider the following imaginary React component written using JSX:
function Header({ links }) {
return (
<header id="my-header" className="header">
<div className="logo">
<a href="/">mtsknn.fi</a>
</div>
<nav>
<ul className="header__links">
{links.map(({ href, text }) => (
<li key={href}>
<a href={href}>{text}</a>
</li>
))}
<li>
<DarkModeToggle />
</li>
</ul>
</nav>
</header>
)
}
The code gets compiled by Babel before it's served to the browser[1],
so that JSX gets replaced with React.createElement()
function invocations[2]:
function Header({ links }) {
return (
React.createElement('header', { id: 'my-header', className: 'header' },
React.createElement('div', { className: 'logo' },
React.createElement('a', { href: '/' }, 'mtsknn.fi')
),
React.createElement('nav', null,
React.createElement('ul', { className: 'header__links' },
links.map(({ href, text }) => (
React.createElement('li', { key: href },
React.createElement('a', { href: href }, text)
)
)),
React.createElement('li', null,
React.createElement(DarkModeToggle, null)
)
)
)
)
)
}
Uh, that's a bit annoying to read.
Let's make that a bit more readable
by assigning React.createElement()
to a new variable h
.
("h" as in hyperscript –
though note that this is not yet proper hyperscript.)
Let's also simplify { href: href }
into { href }
because OCD:
import { createElement as h } from 'react'
function Header({ links }) {
return (
h('header', { id: 'my-header', className: 'header' },
h('div', { className: 'logo' },
h('a', { href: '/' }, 'mtsknn.fi')
),
h('nav', null,
h('ul', { className: 'header__links' },
links.map(({ href, text }) => (
h('li', { key: href },
h('a', { href }, text)
)
)),
h('li', null,
h(DarkModeToggle, null)
)
)
)
)
)
}
Now we can more clearly see that
- the first argument of
React.createElement()
is the tag name or component; - the second argument is the
props
object (ornull
if the element doesn't get any props); - and the rest of the arguments are the children of the element.
So, under the hood, JSX is just regular JS.
And it's not specific to React.
Babel could as well replace JSX with something else than React.createElement()
function invocations.
What hyperscript is and looks like
I couldn't find a proper definition for hyperscript, but think of it as the JS equivalent of JSX. You can use hyperscript to declare the structure of your (React) components in plain JS. Hyperscript is like the code in the previous code block, but better.
Let's take a look at two real-world hyperscript implementations to see what they look like.
Example 1: React with react-hyperscript
Looking at our previous code example, it would be nice if we could at least:
- omit the second argument if it's
null
. This would make the code easier to read by reducing visual noise. - optionally wrap the children in an array. This would make indentation more logical and Prettier-compatible (is that a word?). It would also allow you to use trailing commas if that's your thing.
The react-hyperscript
library
supports that.
It also supports:
- using CSS selectors in the tag names to specify CSS classes and IDs.
For example,
h('header#my-id.foo.bar')
is the same ash('header', { id: 'my-id', className: 'foo bar' })
. - omitting the tag name if it's
div
, i.e. the default tag name isdiv
. For example,h('.logo')
is the same ash('div.logo')
.
Let's see what our component looks like when using react-hyperscript
:
import h from 'react-hyperscript'
function Header({ links }) {
return (
h('header#my-header.header', [
h('.logo', [
h('a', { href: '/' }, 'mtsknn.fi'),
]),
h('nav', [
h('ul.header__links', [
links.map(({ href, text }) => (
h('li', { key: href }, [
h('a', { href }, text),
])
)),
h('li', [
h(DarkModeToggle),
]),
]),
]),
])
)
}
That looks pretty good!
We got rid of null
props and some other prop objects as well by using CSS syntax.
Children that are not on the same line as their parent
are wrapped in arrays.
This makes the indentation clearer.
(Side note:
at the end of the code example,
the DarkModeToggle
component needs to be in an array because
it's a component instead of a string.
There's an open issue on GitHub about that.
Unfortunately the library hasn't received any updates in a couple of years,
but this issue is not a biggie.)
To start using react-hyperscript
,
- run
npm install react-hyperscript
(ornpm i react-hyperscript
for short) - add
import h from 'react-hyperscript'
to your code - start replacing that filthy JSX with the
h()
function.
Example 2: Mithril
Mithril is a small (<10kb gzip) yet mighty JS framework. I prefer using Mithril over React in my own projects because it's so good. (I should write a blog post about it.)
Anyway, Mithril uses hyperscript by default.
Mithril's hyperscript flavor has two main extra features compared to react-hyperscript
:
- Besides CSS classes and IDs,
you can also specify static attributes
in the tag name (first parameter)
using CSS syntax.
For example,
m('a[href=/foo]')
is the same asm('a', { href: '/foo' })
. (Notice how Mithril usesm()
instead ofh()
, but that doesn't matter in practice.) - It's more flexible about its input parameters –
children can be in an array,
but they don't have to.
In our header component example,
the
DarkModeToggle
component doesn't need to be in an array (unlike when usingreact-hyperscript
).
Our component would look like this when using Mithril:
import m from 'mithril'
function Header({ links }) {
return (
m('header#my-header.header', [
m('.logo', [
m('a[href=/]', 'mtsknn.fi'),
]),
m('nav', [
m('ul.header__links', [
links.map(({ href, text }) => (
m('li', { key: href }, [
m('a', { href }, text),
])
)),
m('li', m(DarkModeToggle)),
]),
]),
])
)
}
That's almost identical than when using react-hyperscript
,
only a bit more terse.
(I have highlighted the two lines that are different.)
But being able to specify static attributes using CSS syntax is nifty,
especially when you have more of them;
the example header component has just one.
And the increased flexibility regarding the input parameters
means one thing less to worry about,
i.e. whether children need to be wrapped in arrays or not.
Since Mithril uses hyperscript by default, there's no steps to enable hyperscript. Read the Mithril tutorial and start using it.
Pros and cons of hyperscript
Okay, that's nice and all, but why would you use hyperscript over JSX?
Pros
Here's a non-exhaustive list of the good sides of hyperscript:
- No need for context switching between JS and XML
because hyperscript is just JavaScript.
- Hyperscript could also make it easier to understand
how your JS framework of choice (be it React or Mithril or something else) works.
When learning React and JSX,
a possibly common frustration is
"why can't I use
if
statements andfor
loops in my JSX?" When using hyperscript, it's probably clearer why you can't haveif
statements andfor
loops as function arguments.
- Hyperscript could also make it easier to understand
how your JS framework of choice (be it React or Mithril or something else) works.
When learning React and JSX,
a possibly common frustration is
"why can't I use
- CSS classes and IDs can be specified in the tag name
which leads to terser code.
Very nice when using e.g. Tailwind:
h('img.h-16.md:h-24')
etc.- In some flavors of hyperscript (e.g. Mithril),
other CSS-like shorthands are also possible,
like
m('a[href=/foo].bar.baz')
.
- In some flavors of hyperscript (e.g. Mithril),
other CSS-like shorthands are also possible,
like
- Hyperscript is less verbose (CSS-like tag names, no closing tags). This leads to shorter code – without sacrificing readability if you ask me.
- No need for JSX compilation.
This leads to simpler tooling,
simpler build processes
and fewer dependencies.
- You can start prototyping without having to setup complex build processes et cetera. (Though in the case of React, using Create React App is also easy and quick. But still much more complex!)
- Simpler tooling also means that
your code editor doesn't have to support or have plugins for
JSX parsing, highlighting etc.
Just open
notepad.exe
and start hacking like any advanced programmer.
Cons
-
Hyperscript is not as common as JSX (at least in the React world). Unless you can convince your team to choose hyperscript, JSX is likely the easier choice. If you use JSX, it might also be easier to attract new developers to your team due to familiarity. Though learning hyperscript shouldn't be at all difficult.
-
The syntax might take some time to get used to. While hyperscript is just JavaScript, there are a lot of different kinds of brackets. But like said: learning hyperscript shouldn't be difficult.
-
Because there aren't closing tags like in JSX, you can't see as well where tags end. I haven't personally found this to be a problem though.
-
Android Studio solves this problem nicely for Flutter projects. It automatically shows code comments at the end of widgets (think of React components) like this:
return new MyWidget( child: Container( color: Colors.pinkAccent, height: 200, width: 200, child: Center( child: Text( 'Bacon ipsum', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), // Text ), // Center ), // Container ); // MyWidget
-
-
There's runtime overhead of parsing the CSS-like selectors. This is unlikely a major performance problem unless you have lots of complex selectors, but something to be aware of.
- If you use Mithril,
you can setup
a Babel plugin called
mopt
to statically optimizem()
function invocations. Look ma, no more runtime overhead!
- If you use Mithril,
you can setup
a Babel plugin called
Bottom line
Hmm, looks I listed as many pros as I listed cons. Didn't I say that hyperscript is hyperior?!
If you ask me, the pros greatly outweight the cons. Give my hyperscript over JSX any day, thank you.
In the end it's a matter of personal preference and what works for you and your team. Because this is a personal blog, I'm free to call hyperscript hyperior anyhow, so checkmate, atheists. ;-)
Further resources
- The React docs have a couple of pages about JSX:
- Mithril docs' section about JSX vs hyperscript
hyperscript-helpers
andreact-hyperscript-helpers
offer even terser hyperscript syntax. I'm not a fan of this style, but check them out, you might like them.@thi.ng/hdom
andijk
are hyperscript alternatives based on arrays. I haven't tried these, but they look interesting.
Footnotes
It's also possible to use Babel in the browser to compile code on the fly. That would be slow and in most cases just unnecessary. ↩
React 17 introduced a new JSX transform that makes the output look different. This doesn't change the idea behind hyperscript. ↩