DEV Community

Rickard Natt och Dag
Rickard Natt och Dag

Posted on • Edited on

Testing ReasonML at Sweden's largest property portal, Hemnet

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
}}
Enter fullscreen mode Exit fullscreen mode

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>
)}
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode

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.

Screenshot 2020-05-14 09 36 57

When the compiler is running in watch mode it'll compile file changes even faster.

Screenshot 2020-05-14 09 37 36

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
Screenshot 2020-05-14 09 45 29 Screenshot 2020-05-14 09 46 45

Top comments (2)

Collapse
 
voronar profile image
Kirill Alexander Khalitov • Edited

Thank you for the experiment.
Did you like it? Any improvements in code base quality and architecture, runtime performance, whole UX productivity?

Collapse
 
believer profile image
Rickard Natt och Dag

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.