Each week Hemnet has 2.8 million unique visitors, which is quite a lot in a country with about 10 million inhabitants.
A couple of times per year we have a competence development day where we are allowed to test out new tech or read up on new developments. I chose to integrate ReasonML in our main app.
If you've never heard about Reason, then the new documentation website is a great start https://reasonml.org/.
The experiment
The app is currently a large codebase of Ruby on Rails and React (JavaScript). Therefore, a perfect place to try out some type-safety.
I chose to convert a component that contains a bunch of normal use-cases, e.g. importing other components/images, sending tracking events, and using React context.
Code
These are answers to some questions I got from colleagues about the code.
No import statements
All modules, every .re
file is a module in Reason, are globally accessible. This might seem like an issue, but with good naming and structure, it's perfectly fine.
React.string("text")
React accepts a bunch of types as valid children (numbers, strings, elements, or an array), but since Reason is statically typed everything needs to be mapped to a consistent type. Therefore, we use React.string
to tell the compiler that this string will map to a React.element
. There's a function for each other case React.int
, React.float
, and React.array
.
Pattern matching and option types
In Reason, null
and undefined
does not exist. When doing interop with JavaScript, a possibly undefined
prop will map to Reason's option type, which is either Some(value)
or None
.
{switch (price) {
| Some(price) =>
<span className="mb-2">
<PriceBox price originalPrice />
</span>
| None => React.null
}}
Reason forces us, in a good way, to address all possible states and since the cases of a switch need to return the same type, we return React.null
when price
is None
. In JavaScript we had
{price && (
<span className="signup-toplisting-promo__price">
<PriceBox price={price} originalPrice={originalPrice} />
</span>
)}
Props
In the following example, it might look like the props don't have a value. This is because of punning, which is a shorthand when a variable has the same name as the prop, i.e. price={price}
becomes price
.
let price = 50;
let originalPrice = 100;
<PriceBox price originalPrice />
Bindings to JavaScript code
We were using a Heading
from our component library, so that needed a binding. as
is a reserved keyword in Reason, but not in JavaScript. By adding an underscore in front we can use it in Reason and the compiler will remove it in the compiled code. This is called name mangling.
/* Hemnet.re */
module Heading = {
[@bs.module "@hemnet/react"] [@react.component]
external make:
(~_as: string, ~styleType: string, ~children: React.element) =>
React.element =
"Heading";
};
/* Usage */
<Hemnet.Heading _as="h2" styleType="h3">
{React.string("Raketen")}
</Hemnet.Heading>
For sending tracking events to Google Analytics I created a module that made it clearer what the actual parameters are using labeled arguments. No more need to keep in mind which order the params are supposed to be.
/* GoogleAnalytics.re */
/* Binds to the global variable `ga` */
[@bs.val] external ga: (string, string) => unit = "ga";
let track = (~category, ~action) => ga(category, action);
/* Usage */
GoogleAnalytics.track(
~category="event-category",
~action="event-action",
)
NOTE: The bindings could be made even more type-safe. For example by using variants to only allow specific values to be sent to the JavaScript code.
Testing
Testing remains the same as we can still use the same setup with Jest and target the compiled code.
Metrics
A clean build, running bsb -clean-world
to remove all the compiled code and then bsb -make-world
, compiles the Reason code in about 200 ms.
When the compiler is running in watch mode it'll compile file changes even faster.
This is only for a few modules, but when I've used Reason in larger projects, the longest compile times I've seen for a clean build is ~8-10 seconds. When changing files it's usually well below 400ms.
Final result
The only visual difference is the link color, which is due to a collision between Tailwind (which I also tested out in the experiment) and our global styling. Apart from visuals, the component would now be much safer to use thanks to the great type inference.
Reason Experiment | Production |
---|---|
Top comments (2)
Thank you for the experiment.
Did you like it? Any improvements in code base quality and architecture, runtime performance, whole UX productivity?
Thank you! I really really like Reason and have been using it for some time now! The experiment was mostly for the benefit of my colleagues and for me to share an experience of integrating Reason in a large codebase.
At my last company, we ran two Reason projects in production. We didn't have any type related bugs and us frontend developers loved the experience! The few bugs we had were due to mismatching types when doing backend calls, e.g. a field was nullable that we didn't know about.
Runtime performance is the same as a regular React app, since
reason-react
has zero-cost bindings to React! Apart from that it also feels really good to come back to a Reason project after some time away, because you've got the solid compiler supporting you.