This article was originally written with ReasonML and ReasonReact. I updated it in May 2021 to use ReScript.
ARIA properties are one of the remaining unsolved problems in ReScript. In ReactJS, you can write a component like this: <svg aria-label="Greetings" />
. ReScript, with its stricter syntax, does not allow -
in names. Workarounds exist, but no one has found a canonical solution yet.
You can read the official ReScript docs explanation here. ReScript uses some tricks to map safe versions of names to their JavaScript equivalents, so ariaLabel
compiles to aria-label
. But this trick only applies to DOM elements created by ReScript-React. In other words, ariaLabel
will not magically compile to aria-label
every time itβs used as a prop.
The problem
If you have an external component and your instinct is to write a binding like this, it wonβt work:
module MyIcon = {
@module("some-icon-pack") @react.component
external make: (~ariaLabel: string) => React.element = "MyIcon"
}
<MyIcon ariaLabel="This doesn't work." />
When this compiles to JavaScript, ariaLabel
wonβt transform into its kebab-case equivalent, making it useless.
The solution
It took me much too long to figure this out. As far as I can tell, itβs the most straightforward solution with minimal hassle and runtime cost:
module MyIcon = {
@module("some-icon-pack")
external make: React.component<{..}> = "MyIcon"
@react.component
let make = (~ariaLabel: string) =>
React.createElement(make, {"aria-label": ariaLabel})
}
<MyIcon ariaLabel="It works!" />
Here, the external component isnβt bound like a regular @react.component
, thus making it incompatible with JSX. But then we create a shadow make
function that maps the ariaLabel
argument to the correct syntax. When you run the compiler, the output works just as youβd expect.
Caveats
If you look at the compiled JavaScript, youβll noticed that it isnβt zero-runtime. Our second make
function still exists as a wrapper around the external component. You can avoid this by manually writing React.createElement(MyIcon.make, {})
throughout your project instead of the JSX <MyIcon />
, but Iβm skeptical that the effort would be worthwhile.
You may also notice that the typing for the external component isnβt safe in my example code. Js.t({..})
essentially means βthis is an object with anything you want inside it.β The unsafe typing is fixed by the fact that I annotated the types for my shadow make
functionβs props. However, if you want to use the non-JSX React.createElement
function throughout your project, then you should properly type the Js.t
object in the external binding.
Final thoughts
I hope you find this technique useful for your own ReScript-React bindings. If thereβs a better way of accomplishing this, Iβm happy to learn about it and update this post.
Top comments (2)
There's a neat little trick you can do to make a 'spread' component that just injects the given props objects into its child:
This is a great trick! Thanks.