DEV Community

Yawar Amin
Yawar Amin

Posted on • Updated on

ReasonReact JSX children–a subtle difference from JavaScript

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>
}
Enter fullscreen mode Exit fullscreen mode

It desugars to the following:

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

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>;
};
Enter fullscreen mode Exit fullscreen mode

This would desugar into something more like:

React.createElement("p", undefined, "Hello, " + name + "!");
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)