zth
January 22, 2024, 7:53pm
1
There’s now an implemented RFC for a generic JSX transform (being able to use the built in JSX transform easier with anything not React). A generic JSX transform will allow us to for example plug in other JSX libs easily like Vue/Preact/Hyperons/Kittens and whatever else that might use JSX.
It’s not to be confused with JSX preserve mode , which is another functionality we eventually want to have where the JSX from ReScript is preserved in the JS output so other JS based tools can process the JSX.
Please have a look at the RFC and give any feedback you might have:
rescript-lang:master
← rescript-lang:generic-jsx-transform
opened 11:30AM - 14 Jan 24 UTC
_This is a work in progress. Please feel free to give any feedback._
This gen… eralizes the JSX (v4) transform to not only work on React, but any module that can fulfill the interface needed for the JSX transform.
## Configuration
You configure a generic JSX transform by putting any module name in the `module` config of JSX. This can be _any valid module name_. So you can define your own `MyJsx.res` locally if you wanted to, and it'd still work. Example part from `rescript.json`:
```json
"jsx": {
"module": "Preact"
},
```
This will now put the `Preact` module in control of the generated JSX calls. The `Preact` module can be defined by anyone - locally in your project, or by a package. As long a it's available in the global scope. The JSX transform will delegate any JSX related code to `Preact`.
`@react.component` will still be available, and so is a generic `@jsx.component` notation. Both work the same way.
One thing is important to note - the generic JSX transform has no `version` or `mode` like the React JSX transform has. It's always JSX v4, and the mode is irrelevant (it's `automatic` internally for the generic JSX transform). This doesn't matter for the generic JSX module, it only needs one implementation, but it's good to know.
## Usage example
Here's a quick usage example (the actual definition of `Preact.res` comes below):
First, configure `rescript.json`:
```json
"jsx": {
"module": "Preact"
},
```
Now you can build Preact components:
```rescript
// Name.res
@jsx.component // or @react.component if you want
let make = (~name) => Preact.string(`Hello ${name}!`)
```
And you can use them just like normal with JSX:
```rescript
let name = <Name name="Test" />
```
### File level configuration
You can configure what JSX transform is used at the file level via `@@jsxConfig`, just like before. Like:
```rescript
@@jsxConfig({module_: "Preact"})
```
## Implementing a generic JSX transform module
Below is a full list of everything you need in a generic JSX transform module, including code comments to clarify. It's an example implementation of a `Preact` transform, so when doing this for other frameworks you'd of course adapt what you import from, and so on.
```rescript
// Preact.res
/* Below is a number of aliases to the common `Jsx` module */
type element = Jsx.element
type component<'props> = Jsx.component<'props>
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
@module("preact")
external jsx: (component<'props>, 'props) => element = "jsx"
@module("preact")
external jsxKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsx"
@module("preact")
external jsxs: (component<'props>, 'props) => element = "jsxs"
@module("preact")
external jsxsKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsxs"
/* These identity functions and static values below are optional, but lets you move things easily to the `element` type. The only required thing to define though is `array`, which the JSX transform will output. */
external array: array<element> => element = "%identity"
@val external null: element = "null"
external float: float => element = "%identity"
external int: int => element = "%identity"
external string: string => element = "%identity"
/* These are needed for Fragment (<> </>) support */
type fragmentProps = {children?: element}
@module("preact") external jsxFragment: component<fragmentProps> = "Fragment"
/* The Elements module is the equivalent to the ReactDOM module in React. This holds things relevant to _lowercase_ JSX elements. */
module Elements = {
/* Here you can control what props lowercase JSX elements should have. A base that the React JSX transform uses is provided via JsxDOM.domProps, but you can make this anything. The editor tooling will support autocompletion etc for your specific type when this ships. */
type props = JsxDOM.domProps
@module("preact")
external jsx: (string, props) => Jsx.element = "jsx"
@module("preact")
external div: (string, props) => Jsx.element = "jsx"
@module("preact")
external jsxKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsx"
@module("preact")
external jsxs: (string, props) => Jsx.element = "jsxs"
@module("preact")
external jsxsKeyed: (string, props, ~key: string=?, @ignore unit) => Jsx.element = "jsxs"
external someElement: element => option<element> = "%identity"
}
```
As you can see, most of the things you'll want to implement will be copy paste from the above. But do note that **everything needs to be there unless explicitly noted** or the transform will fail.
## Technical implementation
The technical implementation tries to minimize actual changes (since the React integration is still 1st class and the most refined one). The generic JSX transform is roughly implemented like this:
- Rename relevant JSX transform parts to indicate that they're general rather than tied to React
- No breaking changes to the JSX configuration
- Minimal changes in how the JSX is run. It's all mostly the same as before
## Outstanding questions
- Does Gentype need to be adjusted? If so, how?
- What do we do about React-specific things like ref, forwardRef and so on?
- Should all of the mechanisms around `key` be available to all generic transforms? Is it needed?
## Future
- "Intrinsic elements" (read: The JSX module can define what lowercase elements are available, and what types each of those has). I have a few ideas here that I can detail later on.
- Safer way of mixing JSX transforms in the same project? Right now, everything still needs to be aliased to `Jsx.element`, which means that there's currently no true separation between different JSX at the type level. You could use `React.string` (because that returns a `Jsx.element`) interchangeably with `Preact.string` (because that also needs to return a `Jsx.element`). This isn't ideal, but is pretty niche, so it's something we can solve later on.
## Wrapping up
Excited to hear your feedback on this! This will obviously need a bunch of documentation to be useful.
Personally, I'm excited to ship this even if it's in a reduced form because I believe it can unlock a bit of innovation in ReScript. One example is I'm building [`res-x`](https://github.com/zth/res-x) that uses JSX on the server with ReScript as regular HTML templating. The JSX integration there will be much smoother and idiomatic with this generic JSX transform.
18 Likes
zth
January 31, 2024, 9:47am
3
This is now released (as experimental) in 11.1.0-rc.1
.
Relevant docs here: JSX | ReScript Language Manual
3 Likes
Nice! I’m gonna test out some Solidjs.
1 Like
Exactly at the right time
zth
January 31, 2024, 9:18pm
6
Please report back how it went if you try it.
First things first: It’s working fine!
My first observations:
I had to implement a DOM
module instead of Elements
.
It’s not clear which methods have to be implemented. Some methods have to be implemented in the “root” jsx module and some in the DOM module. Some methos in both.
Well, I could copy all bindings from the docs, but I would like to understand when which method is used. It’s the same in the original react I guess (What is the difference between jsx
and jsxs
, what is the difference between react and reactDOM bindings, etc.).
What framework are you testing out? I’m trying to get this to work with Solid.js.
1 Like
zth
February 2, 2024, 3:49pm
9
A bunch of fixes related to this has landed and will be in the next rc for 11.1. About the docs - yes, a deeper dive into the specifics in the docs would be great, I agree.
1 Like
Tried solid, but had problems with some optimizing steps of rescript (destructuring of signals break their reactivity). Anyways I didn’t want to include babel or some other additional tools.
Then I played with vobyjs (like solid but without babel). Not finished (yet), but could work quite nice. You can see some small examples here: GitHub - dkirchhof/rescript-voby .
Right now, I’m trying out some own stuff. No external deps, no babel, no nothing.
Don’t know how far my journey will go. If I will finish my own lib, using voby or keep working with react
3 Likes
tsnobip
February 3, 2024, 7:21am
11
I’m trying to use email template frameworks in rescript, let’s see how it goes!
1 Like
zth
February 16, 2024, 1:00pm
12
For anyone interested in how the generic JSX transform looks in action, ResX (GitHub - zth/res-x: A ReScript framework for building server-driven web sites and applications. Use familiar tech like JSX and the component model from React, combined with simple server driven client side technologies like HTMX. Built on Bun and Vite. ) is now integrated fully with the generic JSX transform. It uses a modified version of Hyperons to render JSX to HTML on the server.
It defines a module Hjsx
which holds all JSX related things that the transform needs.
It also defined a module H
that binds to the render-jsx-to-string functions.
You then configure rescript.json
to use Hjsx
as the module, and set up a few relevant open
: https://github.com/zth/res-x/blob/main/demo/rescript.json
Viola, now you can use @jsx.component
to create JSX components, and use Hjsx.string|int|array
etc to render JSX elements.
Side note: It comes with its own custom DOM props with a few interesting bits of “magic” baked in, like HTMX props driven by variants and a custom mini fw for doing simple declarative DOM updates .
6 Likes
Hey,
I have a custom function which should create a jsx element
type options<'props> = {
name: string,
render: 'props => Jsx.element,
}
let makeWithProps = options => {
...
}
module TestComp = {
type props = {message: string}
let make = makeWithProps({
name: "test-comp",
render: props => <div> {JSX.string(props.message)} </div>,
})
}
Now I would like to create components without props like this:
module TestComp2 = {
let make = makeWithProps({
name: "test-comp",
render: () => <div/>,
})
}
Unfortunately, the compiler complains about the props type (unit). Only records are allowed.
Empty record literal should be annotated or used in a record context.
My workaround is to define a second make function:
type empty = {}
let makeWithoutProps = (options: options<empty>) => {
...
}
Which is used like this:
module TestComp3 = {
let make = makeWithoutProps({
name: "test-comp",
render: _ => <div/>,
})
}
TLDR;
Is it possible to define JSX.elements with unit
as props
type?
I think you should use an empty record for such cases.