DEV Community

loading...

ReasonReact JSX children–a subtle difference from JavaScript

Yawar Amin
Programming languages enthusiast. Author of Learn Type Driven Development: https://www.packtpub.com/application-development/learn-type-driven-development
Updated on ・2 min read

RECENTLY, I realized that the way we concatenate strings as children elements inside ReasonReact JSX ends up producing subtly different React output than in equivalent JavaScript JSX.

For example, here's a JavaScript React component:

function Greet({ name }) {
  return <p>Hello, {name}!</p>
}

It desugars to the following:

function Greet({ name }) {
  return React.createElement("p", undefined, "Hello, ", name, "!");
}

Note how the JSX desugar process breaks up the templated text version into a variadic argument list. There is no string concatenation going on here.

In contrast, what we normally do in ReasonReact is:

module Greet = {
  [@react.component]
  let make = (~name) =>
    <p>{React.string("Hello, " ++ name ++ "!")}</p>;
};

This would desugar into something more like:

React.createElement("p", undefined, "Hello, " + name + "!");

Notice the difference? The element children are no longer a variadic list of strings, but a single concatenated string. To match the JavaScript JSX version, we would have to write the children like:

<p>
  "Hello, "->React.string
  name->React.string
  "!"->React.string
</p>

Is this a big deal? Probably not, if you're not doing a lot of string concatenation! Or unless you have some code that introspects element children and behaves differently depending on what the children are.

ReasonML strongly-typed JSX–a deeper look

The divergence is happening in the ReasonML JSX implementation. In Reason JSX, every element is strongly-typed and there's no built-in interpolation. In JavaScript JSX, for example, you can do:

<p>Hello, {name}!</p>

But in Reason JSX that's not syntactically valid. The syntax rules say that every JSX element must contain zero or more JSX elements, and some raw text Hello, {name}! is not parsed as a JSX element. It needs to be cast into a proper JSX element type somehow. In ReasonReact, that type is React.element, and the built-in functions that cast things are:

  • React.string: casts a string into an element
  • React.array: casts an array of elements into an element

(There is a merged PR to add casts from int and float to React.element, which is legal because of the underlying JavaScript implementation. It should be published in a future release but in the meantime you can implement it yourself if needed.)

So that's why we need do do the explicit casts if we want the same output React components:

<p>
  "Hello, "->React.string // These casts are zero-cost
  name->React.string // Because they are identity externals
  "!"->React.string
</p>

Again, this may not actually matter too much in practice. BuckleScript offers a pretty nice way to do string interpolation, built-in:

<p>{j|Hello, $name!|j}->React.string</p>

But it's helpful to know in case you ever bump up against unexpected children elements.

Discussion (0)