Description
I'm proposing a generic JSX transform where you bring your own JSX assets (functions to call, types for JSX nodes, etc), that ReScript can delegate to. This will both enable slicker integration with other FE frameworks than React that also use JSX, and enable innovation since JSX is just syntax sugar for function calls.
JSX is an XML-style syntax that's great for templating hierarchical things like HTML, while preserving the full strength of ReScript in your templating. With a generic JSX transform, one could leverage the fact that JSX is part of the ReScript language to build your own things leveraging ReScript's great JSX support. I believe this could unlock some innovation.
Background
We currently have a great React JSX transform in ReScript, including the @react.component
transform for component support. This works well for React. It's possible to use the React JSX transform with other frameworks today by carefully overriding the React
, ReactDOM
and Jsx*
modules. However, this is cumbersome, error prone, and is a hack.
Prior art
TypeScript let's you configure various things related to JSX: https://www.typescriptlang.org/tsconfig#jsxFactory
Proposal
I propose a simple way of swapping out the React JSX transform to your own custom one, and additionally a dedicated @jsx.component
transform.
Configure a custom JSX module in bsconfig.json
{ "jsx": { "customModule": "MyJsxModule" } }
MyJsxModule
is then what the JSX PPX delegates to for all JSX related functionality.
@jsx.component
A generic component transform that gives the same ergonomic experience as @react.component
, but with none of the React specific quirks. We'll need to figure out which of the quirks belong in the generic PPX.
Complete example
Here's a sketch of a complete example of how this could look. Note: Hyperons is a lightweight lib for rendering JSX to a string of HTML.
bsconfig.json
{ "jsx": { "customModule": "Hyperons" } }
// Hyperons.res
type element
type domProps = {
...JsxDOM.domProps,
@as("my-custom-dom-attribute") myCustomDomAttribute?: [#on | #off]
}
type component<'props>
type fragmentProps = {children?: element}
@module("hyperons/src") external jsxFragment: component<fragmentProps> = "Fragment"
@module("hyperons/src")
external jsx: (component<'props>, 'props) =>element = "h"
@module("hyperons/src")
external jsxs: (string, domProps) =>element = "h"
external someElement:element => option<element> = "%identity"
@val external null: element = "null"
external float: float => element = "%identity"
external int: int => element = "%identity"
external string: string => element = "%identity"
external array: array<element> => element = "%identity"
@module("hyperons/src")
external render: element =>string = "render"
// SomeComponent.res
@jsx.component
let make = (~status) => {
<div myCustomDomAttribute=status />
}
// App.res
let app = <SomeComponent status=#on />
let html = Hyperons.render(app)
Ending thoughts
It'll be important to ensure that all of this works seamlessly, including things like autocomplete via the editor integration for both user defined components and lowecase "HTML" dom nodes.
Interested in hearing any feedback really.