JSX v4 & next rescript-react 0.10.x

Hey all,

This post is intended to give recent work more visibility and coordinate efforts for the potential JSX changes and updates on @rescript/react.

JSX

The compiler introduced a very crucial feature that may help us to drastically reduce the logic of the JSX / @react.component transforms. We are currently in the exploration and planning phase on how the new JSX transform could look like.

The GH issue for the meta discussion can be found here: https://github.com/rescript-lang/syntax/issues/521. We still need to assess all the requirements / wishes the new transform should bring, without introducing any breaking changes. So feel free to join the discussion.

Later on we may want to put the results of our explorations into a proper document (maybe in form of a PR to the syntax repo?) to persist the design intentions.

General goals / constraints for the new JSX design:

  • Remove as much magic / use the language as much as possible (makeProps, make may not be needed anymore?) → less magic = less complexity = better tooling support and maintainability
  • Make sure JSX v3 code mixes gracefully with JSX v4 code. Existing code should compile as usual, even with different settings enabled
  • JSX v4 should ideally work without any additional changes to rescript-react.

First explorations on the new transform can be found in this PR: https://github.com/rescript-lang/syntax/pull/517 (the title may be a little misleading).

In later iterations, we’d like to explore how to make the JSX more flexible for other frameworks, and even consider JSX related settings similar to TS (e.g. emit settings like preserve). The new React transform and fulfilling fast-refresh requirements will also be part of that.

rescript-react

There were some blockers for rescript-react until we shipped the new JSX. Now that the JSX plans are re-organized, I think we should move on with new 0.10.x releases, and accept new contributions to e.g. support React 18 features OOTB.

I am not invested in any of the new React@18 functionality myself (I am mostly stuck on v16 rn and Ricky is a little too busy to help out here), so I am happy for any changes from contributors that use React@18 and ReScript that are reliant on the code. (\cc @tom-sherman / whitchapman who recently created PRs on that front). We should also make sure that the React docs on rescript-lang .org are in sync with these changes.

PR by Tom: https://github.com/rescript-lang/rescript-react/pull/35
PR by Whit: https://github.com/rescript-lang/rescript-react/pull/46

^-- not sure what the states are here, so more coordination may be required?

Please use the corresponding issues / PRs for input if applicable.

15 Likes

Great writeup!

Does JSX v4 intend to allow the new JSX transform from React? ie. move away from createElement

Regarding React 18 PRs: Definitely need some coordination here. There are some unresolved comments on my PR and Whit adds some good changes in theirs. I’ll see if we can land these separately as stacked PRs.

This does require my PR being merged first and I should have some time to look at resolving some of the comments today actually.

7 Likes

That work on the JSX PPX looks awesome! I was slightly skeptical seeing just the PR opening post but reading the full discussion it’s a great improvement that very nicely fits in with slight language improvements! That’ll be quite the win for onboarding new people to ReScript and will force me to revisit a previously written blogpost.

We’re currently developing against React 18 with their streaming API so if help is needed with the PRs then I’m happy to make some time next week (unless someone beats me to it).

7 Likes

Does JSX v4 intend to allow the new JSX transform from React? ie. move away from createElement

Only for the Uppercase JSX, yes it is.

<Foo id="2" name="Foo" />

// generates

Foo.make({id: "2", name: `Foo`}),

But, for the lowercase, it keeps v3 way to avoid breaking changes with rescript-react.

By new JSX transform I mean this one: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

I’m slightly concerned about generating Foo.make(...) as a JSX child with regards to stack traces, profiling, and other component stacks. Could you please document what this looks like when for example an error is thrown inside of Foo render, is this as traceable as the current ppx?

This may be a question for @cristianoc actually as I can see that they have specced this behaviour in https://github.com/rescript-lang/syntax/issues/521

Since 18 was released, totally forgot 17 :grinning_face_with_smiling_eyes: I think it is an improvement. My concern is any chance to break the existing codes. Let me test with ppx little bit first.

I agree with @tom-sherman. I don’t think

is correct either.

I used babeljs.io to check what babel does. With the old JSX transform,

<Foo id="2" name="Foo" />

gives

React.createElement(Foo, {
  id: "2",
  name: "Foo"
});

and with the new one

import { jsx as _jsx } from "react/jsx-runtime";

_jsx(Foo, {
  id: "2",
  name: "Foo"
});

I am not really able to say offhand what exactly might break if not doing it the “official” way though.

One case where transforming to

Foo.make({id: "2", name: `Foo`})

is going to break for sure is if Foo.make is not implemented in ReScript, but is an external definition for a class-based component implemented in JS.

Thank you for your feedback!

I’m not sure of it. IMHO, there are two cases, 1) component written in ReScript, 2) component bound with external. I think both will have Foo.make, therefore it will not break.

Anyway I’m testing Foo.make({ ... }) without or with React.createElement, _jsx` in sample projects. The spec will be updated as per the result.

I was referring to class-based components, i.e. something like this (untested):

JS:

export default class Foo extends React.Component {
  render() {
    return <p>Foo</p>;
  }
}

ReScript:

module Foo = {
  @react.component @module("./Foo.js")
  external make: unit => React.element = "default"
}

@react.component
let make = () => <Foo />

Also, I think hooks are going to break if invoking function-based component directly instead of using React.createElement, as described here: Don't call a React function component

Please follow what the babel plugin and the current PPX version are doing and keep using React.createElement.

2 Likes

Thank you @cknitt this in fact removes one of the questions of where React.createElement should be called: definitely in the application, so no use doing it in the definition.
(Under the assumption that bindings to externals are zero cost).

Added items to explore based on feedback here:

What if the V4 allows only the new JSX transform for React, what are the projects being affected or broken? Making it configurable will be the best solution I guess. But I want to know if there are many cases that can’t upgrade the React version or anything else?

I have an initial idea to make user interface as below. The react-runtime config key from babel configuration, but I’m not sure of automatic. The semantic of ‘automatic’ might give a wrong message, which is that ‘automatic’ will gear the new jsx transform depends on the project environment automatically. Any idea?

// bsconfig.json
"reason": {
  "react-jsx": 3 // or 4
  "react-runtime": "classic" // or "automatic"
}