Goodbye JSX, Hello Comma-First Hyperscript
I remember when I first started using React in late 2015. There was a debate going on about whether or not JSX was a good thing. I don’t know that there’s a definite answer, but all the examples used it, and it’s still the predominant way of writing React components.
Here’s an example of JSX that I got from the Redux Todo example.
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
)
JSX Pros:
- It works.
- It’s reminiscent of HTML, and HTML is declarative.
JSX Cons:
- It requires continuous context switching between HTML and JavaScript.
- Curly brackets are used as JS objects and also to switch between HTML and JS.
- If you pass children to an element, you have to write the element name twice, and component names can be very long.
- Even if the attribute name matches the value’s variable name, you have to write each, unless you group attributes into an object and use destructuring.
- = is used for attributes and arrow functions
- Commenting-out code is not easy
- You end up rewriting tags as self-closing if you remove the children (related to item 3).
- It has nested HTML/JS contexts (JS inside of HTML inside of JS inside of HTML).
I would have added “requires compilation,” but you’re going to be doing that anyway, so it’s not really a con.
There are 2 other front-end JS frameworks that I’ve used which use a functionally-reactive paradigm similar to React, but they have a different convention for rendering components with JavaScript. They use functions that are basically React’s createElement function. The frameworks I’m talking about are Mithril and Skatejs. Mithril uses “m”, for Mithril, and Skate uses “h”, for hyperscript, but in actuality Mithril supports hyperscript while React and Skate do not. By actual hyperscript, I mean the ability to include classNames and IDs inside of the element string.
// mithril
m("div#my-container", { data })// skate
h("div", { id: "my-container", data })// react
createElement("div", { id: "my-container", data})
For our purposes, let’s just consider hyperscript as a way of describing the view using functions like the ones above. The arguments are in the order of element, attributes, children. Here’s a small example that includes children.
h("p", { id: "uncreative-example-text" },
h("span", {}, "Hello"),
h("span", {}, "World"),
)
So, having become a bit weary of the cons that I mentioned for JSX, I started bringing this convention into my React code. Let’s re-write our JSX example using this style.
import React, { createElement as h } from "react"const TodoList = ({ todos, onTodoClick }) => (
h("ul", {},
todos.map(todo =>
h(Todo, {
key: todo.id,
...todo,
onClick: () => onTodoClick(todo.id)
})
)
)
)
Now, if you’ve never seen this syntax for rendering with JS, it might not look any better than JSX, but consider that all of the cons we listed above have just vanished. Brackets are for objects only, and there’s no context switching. We’re in JavaScript the whole time. It’s still not great though. It gets messy when you have a lot of attributes and children. Keeping track of the closing parentheses can be a lot of work too. So where do we go from here? Enter Elm.
I’m not going to go into detail about Elm. You should definitely check it out. One convention of coding in Elm is using “comma-first syntax.” Elm also has view functions similar to the hyperscript examples above, and comma-first syntax is also used in the view functions. Elm is not JavaScript, but we can do that too.
To show you what I mean by comma-first, in case you haven’t seen it, here’s an example of a JS object written in comma-first syntax.
{ name: "Jez"
, group: "The ol' dude brothers"
}
You can see this used in the npm documentation. One thing to note is that the main object and all nested objects always start and stop at the same indentation. And all the object items, except the first, start with commas at that same indentation, so you can scan down and immediately see the start, stop, and items of an object. This will come in handy when we use it for hypertext.
Let’s rewrite the hypertext example to use comma-first syntax.
import React, { createElement as h } from "react"const TodoList = ({ todos, onTodoClick }) => (
h("ul", {}
, todos.map(todo =>
h(Todo
, { key: todo.id
, ...todo
, onClick: () => onTodoClick(todo.id)
}
)
)
)
)
It’s a bit difficult to see the advantage on a small component, so I’ll include a larger example, with loops, conditionals, etc. Let’s say we are making a timeline that has historical events on it. This might be what it looks like to render one item on the timeline.
const HistoryItem => ({ name, type, expandable, ...rest }) => (
h(TimelineItem
, { label: h(TimeLineItemLabel, { name })
, maxContentWidth: "210px"
}
, h(Card
, { style: { backgroundColor: "transparent" }
, expandable
}
, h(CardTitle
, { title:
h("span", { style: { fontSize: 20 } }, type)
, subtitle:
h("span"
, { style: { fontSize: 12 } }
, `by ${rest.owner.name}`
)
, style: { padding: "10px 10px 10px 15px" }
, actAsExpander: expandable
}
)
, expandable &&
h(CardText
, { style: { padding: "10px 8px" } }
, rest.contents.map((content, contentIndex) =>
h(TimeLineItemContent
, { type, content, key: contentIndex }
)
)
)
)
)
);
Notice that if you ever need to comment out a piece of this, you can just select the block and hit your keyboard shortcut for commenting and BAM! It’s way easier than commenting in JSX. And even though we have a very complex nested structure of element function calls, it’s very easy to tell where an element starts and stops, and what are it’s children.
Anyways, I’ve been writing React like this for a couple of months now, and I absolutely love it.