Bug/question: using custom JSX functions
I have a question, but it could potentially be a documentation bug. Also, I've asked my question in three different venues and I think that maybe it's too low-level for most people to answer, so I'm reposting it here; I hope that's OK.
I'm brand new to Reason/ReScript and I'm evaluating how our team might use Reason in our React project.
We've hit one major stumbling block. Our project uses Theme UI for CSS-in-JS styling, which works like this:
- Each styled component gets a special
sxprop that contains a style object. E.g.,<div sx={{ margin: '4px' }} />. - Each
.jsx/.tsxfile that hassxprops in it is marked with a special JSX pragma at the top (e.g.,/** @jsx jsx */), which tells Babel to usejsx(...)calls instead ofReact.createElement(...). This behavior is part of Babel, not Theme UI, and you can use whatever function you like (in Babel's docs, they usePreact.h). Theme UI'sjsxis a function likeReact.createElementthat also knows how to handle thesxprop. It's imported from Theme UI like so:import { jsx } from 'theme-ui'.
So basically, I need to figure out how to use Theme UI's jsx function to create my elements rather than React's createElement. I also need to figure out how to get sx into the prop type for JSX elements, but that's another battle.
My first hunch was that the limitation had to do with ReasonReact, so I opened an issue.
I realized later that the limitation is at a deeper level, with something called the JSX ppx. It appears that ppx's are an OCaml mechanism for extending the language through AST preprocessing?
I looked up that particular ppx. It appears that calls to React.createElement are hardcoded, but it's hard to tell if that's a default or if it's the only option.
Then, I came across this bit of documentation in the ReScript manual:
For library authors wanting to take advantage of the JSX: the
@JSXattribute above is a hook for potential ppx macros to spot a function wanting to format as JSX. Once you spot the function, you can turn it into any other expression.This way, everyone gets to benefit the JSX syntax without needing to opt into a specific library using it, e.g. ReasonReact.
The docs are unclear here, hence why I wrote that perhaps there's a documentation bug. On the one hand, the docs imply that I can somehow leverage "the @JSX attribute" provided by the ppx to transform my JSX into JS using a custom function--that's exactly what I'm looking for. On the other hand, it implies that I need to write my own ppx to get this to work, defeating the purpose of leveraging all the work already put into the JSX ppx.
I'm also not sure what "spot the function" means in this context. I naively tried writing @JSX div(~onClick=handler, ~children=list{child1, child2}, ()) and got this error: Unbound constructor JSX.
I tried searching the ReasonReact source code for any mention of "jsx" and I couldn't find any. I see the "reason": { "react-jsx": 3 } in bsconfig.json, but I'm not sure how the ppx in ReScript and ReasonReact are connected, nor how I could leverage the ppx to support my own use case (i.e., Theme UI's jsx function).
Does anyone have an idea for how to tackle this problem? Thanks!
@bobzhang @rickyvetter, git blame suggests that you're most familiar with that ppx. Sorry to single you out 😂. Any insights?
Anyone?
I've made some progress. Here's what I know so far.
JSX transformation takes place in two phases:
- Rescript replaces raw JSX with calls to
[@JSX] ...injscomp/js_parser/jsx_parser.ml. So<div props1={a} props2={b} />becomes[@JSX] div(~props1=a, ~props2=b, ()). This is a kind of intermediate format that's intended to be further transformed into something specific to a particular JSX-based framework, e.g. React. [@JSX] ...calls get replaced with ReasonReact-specific calls byjscomp/syntax/reactjs_jsx_ppx.cppo.ml. So[@JSX] div(~props1=a, ~props2=b, ())becomesReactDOMRe.createElement("div", ~props={"props1": a, "props2": b}).
The first phase is built into the Rescript parser itself. I believe that it executes no matter what.
The second phase is hooked into as a PPX here and here (it gets built as the module Reactjs_jsx_ppx_v3 here). Adding {"reason": "react-jsx": 3} triggers the PPX and the second transformation.
So basically, what needs to be done is:
- Write a PPX that replaces the output from the first phase with calls to a different kind of
createElement, like Theme UI'sjsxfunction. Perhaps this is as easy as search/replaceReactDOMRewithThemeUI, then declare an external functioncreateElementon aThemeUImodule that points to Theme UI'sjsxfunction. - Write bindings for Theme UI.
- Wire up the new PPX to the build process.
Hey @earksiinni - your research seems to be pretty solid - congrats on digging through this. A lot of the reason this is under-documented is because PPXs are very hard to write in a way that makes the developer experience great, so we don't really want to encourage a ton of them.
Assuming that you want to match the JS/TS style behavior then you'll need an additional step which is an actual pragma switch. Sharing one potential example of how that might look here (astexplorer is a great tool for looking at the actual ast output but only supports OCaml and Reason syntax today) - https://astexplorer.net/#/gist/708dd752ed77f2aeab9d88d0e8a42703/4475dfafca43d71c291cb6f7293c2876a88e6fa6
Then your PPX needs to respect that pragma and allow code to fall through to the default PPX compilation if it doesn't see it (or if you're using <UpperCase/> components or something).
Feel free to DM on Discord or post more questions here.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.