During the ReScript Retreat, we achieved an initial (rough) version of preserving JSX code in the JavaScript output. This is not the final version we plan to ship in v12, and we welcome your feedback to determine its viability and identify any remaining gaps.
Installation
You will need the latest v12 version from the master branch of the compiler. Install it via the pkg.pr URL:
npm i https://pkg.pr.new/rescript-lang/rescript@362a1da
The suffix is optional; you can choose to use the .jsx extension or not based on your use case. The -bs-jsx-preserve flag will likely be moved to a dedicated setting under the jsx node. You need both jsx.version: 4 and -bs-jsx-preserve.
Known Limitations and Open Questions
We are aware of several limitations, and depending on your feedback, we will address them:
The JSX output is not formatted neatly. Props are displayed on the same line, and children are not indented.
Currently, all files share the same extension. Changing to .jsx might seem unusual if your file doesn’t contain any React components. In large projects, this may be mitigated by using preserve mode only for specific packages in the mono repo.
There may still be some edge-case bugs in the JSX output; please report any you encounter!
React Compiler
I tried this in my own project and can confirm that the React compiler worked as expected.
Solid
This new feature should enable the use of Solid. We haven’t tested this yet, so we would welcome a community sample!
Nice looking forward to hear more about the retreat.
Currently, all files share the same extension. Changing to .jsx might seem unusual if your file doesn’t contain any React components. In large projects, this may be mitigated by using preserve mode only for specific packages in the mono repo.
Have you considered a new file extension like *.resx?
So in your example it could compile *.res files to *.res.js and *.resx files to *.res.jsx. resx files will preserve jsx mode and res files wouldn’t.
I created a test app with npx create-rescript-app, switched to rescript v12 and replaced react with solidjs.
The first impression is very promising. The app compiled without problems and solidjs could handle the generated jsx files well .
So preserve mode seems to be working with solidjs, which is great!
But sadly, I immediately ran into a problem with reactivity. This is not related to JSX preserve mode, but would still require an additional transformation step to make the code work.
In the example code there is a button component:
let make = props =>
<button
{...
props}
className="bg-blue-600 text-white"
/>
that is compiled to:
function make(props) {
let newrecord = {...props};
return <button {...newrecord} className={"bg-blue-600 text-white"}/>;
}
The line let newrecord = {...props}; breaks reactivity since it basically copies all properties.
The line only appears when additional classes are added to the button component.
This button:
let make = props =>
<button {...props} />
would compile to:
function make(props) {
return <button {...props} />;
}
and works just fine.
I have to do further tests to see if there are any other problems, but still, getting rid of the “js-back-to-jsx” babel-transform, that is currently necessary, is a great step forward.
Glad to hear some progress is made! Thanks for diving back into it.
Is spreading props common in Solid? I guess what’s going on here in terms of ReScript is that JSX props are represented as records, and record updates are immutable. So, it needs to create a new record to be consistent with the expectations.
EDIT: Actually, I don’t see the reason for that happening either. That immutable update shouldn’t happen in preserve mode I guess.
I just checked and it looks like the problem with the lower case component names is gone (custom components would result in different code than a standard component created with jsx.component). All components have upper case names now.
There was another problem with switch statements and solidjs signals. Here, the resulting code is slightly different (the curry._1 call is gone, which is to be expected since the change to uncurried), but the resulting code still contains an additional variable assignment that breaks reactivity:
@react.component
let make = () => {
let maybe = createSignal(Some("option"))
<div>
{
switch maybe() {
| Some(m) => m
| _ => ""
}->React.string
}
</div>
}
compiled code:
function Broken(props) {
let maybe = SolidJs.createSignal("option");
let m = maybe();
return <div>
{m !== undefined ? m : ""}
</div>;
}
The problematic part: let m = maybe();.
But I am not sure this can be “fixed” (from a solidjs perspective), since, from the compilers perspective, it is a meaningful optimization and it totally makes sense to do it.
And, as you already pointed out in another post: There is a perfectly usable workaround available, by using the <For>, <If>, <Show> components provided by solid.
So all in all I would say, we only need to solve the very first problem with let newrecord = {...props}; and everything should be fine.
Great! But to confirm - that same code in TS (as in - assign the maybe() call to a variable, do type assertion on that variable, and then use the refined result) would have the same issue, no?
Yes that would be problematic too. For these cases, solidjs provides the createMemo function that can be used to create a derived signal that keeps reactivity.
You can theoretically do hat in ReScript too. The difference is that in ReScript, the problem is much less obvious, since the source code looks completely normal, and you wouldn’t get an error. The component would simply never update. So it’s much harder to spot the problem. But once you understand it, there are multiple solutions available that all work well in ReScript.
Just out of curiosity, could you show an example of ReScript vs TS code where this is the case (much harder to spot the problem in ReScript vs TS)? Would be interesting to see if we can do something more about it.
By calling count() inside of span, solid figures out that it needs to re-render <span> anytime that count changes
In @Fattafatta’s example, in the rescript code they put maybe() in between the <div>, which is correct, but the compiler put the maybe() function call outside of it in the output, thus calling the getter outside of the tracking scope
Right, thanks, and I follow all of that. My question is about contrasting TS and ReScript code, and specifically where the ReScript code is much less clear than the equivalent TS code. I’d like to see how the TS code looks when it does the same thing (take a value, refine the type of it so you can use it, and then use it).