Why are text nodes in jsx interpreted as symbols?

In javascript, <span>Hello world!</span> is a jsx value specifying a span element with a text node as a child; <span>Hello {place}!<span> interpolates a variable into that text node.

In rescript, <span>Hello world!</span> is a syntax error, and <span>greeting</span> works as long as greeting is a variable containing a React.element. This reversal would maybe make sense to me if I could also do <span>toGreeting(place)</span> or some other expression, but it seems the only thing allowable in that position without enclosing braces is a bare symbol.

This makes me wonder why the jsx syntax has this variation in rescript compared to javascript. What motivates this? I don’t think it’s some demand of the type system, since as far as I can think of, text nodes would trivially compile to React.strings. I also can’t think how it could be problematic syntactically.

For me, one of the advantages of jsx/HTMl templating is being able to write things like <span>This is an <em>important</em> message</span>, whereas in Elm, Halogen, or any other framework where VDOM values are built with regular function calls, you have the additional annoyance of chopping it up into text nodes and an em element. This same annoyance comes up in rescript’s jsx where I have to do <span>{React.string("This is an ")}<em>{React.string("important")}</em>{React.string(" message")}</span>, thinking very carefully about the spacing in the strings.

3 Likes

No answers from me but its really always baffled me that JSX, which is designed to make HTML in code much more convenient, takes a turn for the worse in rescript, and makes the simple string a syntax nightmare. Its my opinion that half the people from the JS/TS world, interested in rescript, take one look at this and run away.

1 Like

This is because things can only ever be of 1 type in ReScript.

If you look at JSX as a function you can think of it like this:

// jsx
<div>hello</div>
// function div("hello")

// jsx
<div>42</div>
// function div(42)

JavaScript and TypeScript are fine with string | number | null | undefined | React.element, but in ReScript a child can only be React.element, which means we need a function to convert a string to a React.element.

It might seem weird at first, but you get used to it very quickly especially if you do it with a pipe operator.

<div>{"hello"->React.string}</div>

It’s all part of the type system.

For one of my recent projects I have been using a markdown library to handle things like bold, italics, etc…

// nose.res
let intro = `
# Nose
Before you take a sip put your nose into the glass.

What do you smell?
`

@react.component
let make = () => {
  <main>
    <Markdown> intro </Markdown>
  </main>
}

I wonder whether we could use coercion here as well? @zth?

But in html, everything between tags IS a string. If you type it in your editor between tags, it already is a string. And then the browser displays it as a string, its only rescript that is pretending it could be something else. JSX is already a huge heap of sugar, I don’t see any reason it just cant treat anything between tags as a string by default.

Some type of coercion here would be nice. I personally have adjusted to it, but @kswope makes a good point that this is weird to people coming from JS, which is our target audience.

Just to follow up. From the docs

let wrapChildren = (children: React.element) => {
  <div>
    <h1> {React.string("Overview")} </h1>
    children
  </div>
}

Isn’t the fundamental problem that rescript/reason decided to do it completely backwards of every other templating system I’ve ever used?

This is what most people would expect because it prefers concise strings over concise variables.

let wrapChildren = (children: React.element) => {
  <div>
    <h1> Overview </h1>
    {children}
  </div>
}
5 Likes

This would be actually much more convenient to use. And I personally know people who didn’t like ReScript only because of React.string

3 Likes

I understand the confusion coming from JavaScript, but I still find it odd that people consider it such a sore point. In any serious app you would need to support multiple languages, which means you need translation infrastructure, which means you will never use raw strings in your app anyway. And sure, toy projects are what people would typically start out with and experience first, but I’d rather have the language optimized for serious apps than toy projects.

Or maybe single-language apps really are a lot more prevalent than I think they are…

6 Likes

But I don’t think that it’s impossible to make typesafe. For example we already have template literals where you can use ${} to embed a string. Isn’t it pretty much the same case, but instead of ${} we have {} and the embeded type is React.element?

This. Even if you start with React.strings (or some shorter aliases) everywhere, they’re much easier to replace with some localization helper than just raw “html-like” strings.

3 Likes

WTH, You just called every web app I’ve worked on or built in the last 20+ years a toy project, even though they make money or were for major corporations.

1 Like

This. Even if you start with React.strings (or some shorter aliases) everywhere, they’re much easier to replace with some localization helper than just raw “html-like” strings.

I’d safely bet that most apps are not some international success stories (or swiss) but internal corporate apps or b2b apps that have no use of i18n. Then there are apps that are targeted to a certain nationality or region that have no use of i18n. I’d say i18n is the rare exception for the totality of web apps and shouldn’t be the rational for a hassle for the majority of web developers.

2 Likes

Even if something isn’t localized text and content for anything I’ve worked on comes from a CMS, or in some cases a JSON file. I’ve rarely hardcoded text.

The app I am working on now uses markdown for all of the content.

1 Like

I too work on a lot of internal company tools and we hardcode our strings, and I also wish I didn’t need to use React.string

This is because things can only ever be of 1 type in ReScript.

If the react compiler can convert <div> into functions and correctly parse the types of <div className> safely, can it also be smart enough to figure out that anything between tags not inside a {} is a React.string? Even if you still needed to convert non strings this would be a huge improvement on DX:

let counter = (
  <div>
    You've clicked it {React.int(count)} times!
  </div>
)
3 Likes

I highly agree. Also, it’ll make it look more similar to how JS work, allowing to easily copy-paste code back and forth.

This exactly. As far as I can tell, this is a single radical change which Rescript made to the JSX syntax, and to me it looks like nothing but a footgun.

From this thread it sounds like the majority opinion is in agreement.

For one of my recent projects I have been using a markdown library to handle things like bold, italics, etc…

That’s what I’ve had to do in the past when using Elm, but that’s precisely why I like JSX, because I don’t have to go looking for third-party solutions to formatting large blocks of text, and I don’t have to give the client extra work parsing and rendering the markdown.

In any serious app you would need to support multiple languages

In my workplace we have a b2b product with a strong customer base, which has been evolving for 20 years, and >3 million LoC all-in-all, and we’re just starting to think about i18n. I think there is a huge gulf between “toy project” and “requires i18n” you’re missing.

1 Like

I think what is happening is that it wants plain strings inside tags to be variables, so you can do

<div>x</div> 

instead of

<div>{x}</div>

You can see this in the example from the docs. This doesn’t look like a necessity of the type system but rather an arbitrary choice.

let wrapChildren = (children: React.element) => {
  <div>
    <h1> {React.string("Overview")} </h1>
    children
  </div>
}
1 Like

I see it as a small price to pay to completely eliminate “Objects Are Not Valid as a React Child” and preventing other runtime errors at compile time. My experience from onboarding multiple engineers to ReScript from 0 prior exposure is that it takes very little time to get used to.

It isn’t really a change to JSX syntax. All expressions inside JSX must be inside an expression container:

You can see this quite clearly using astexplorer

The example you have put looks more like a printer bug IMO than an indication the JSX PPX parser already handles JSX Children parsing to a union of JSXText / JSXExpressionContainer rather than the React.element type

I think measuring forum activity as a gauge of community consensus is a mistake–my experience has been people are busy building with ReScript and may not necessarily check in all the time.

1 Like