DEV Community

Florian Hammerschmidt
Florian Hammerschmidt

Posted on • Edited on

Reason(React) Best Practices - Part 1

After using Reason with React for nearly 2 years, I decided to hold a talk about best practices with Reason and ReasonReact at the ReasonML meetup in Vienna (@reasonvienna). Given that it was my first tech talk in a tech meetup, it went pretty well. I published the slides that same day on Twitter, but slides unfortunately do not tell the full story. So I decided to give it a go and put it here in a more descriptive form. I also split it up a bit, as the talk took about one hour and I prefer to keep blog posts short and to the point.

But enough waffling, let's get into it:

The identity trick

Zero-cost React.strings!

With Reason being a 100 % type-safe language, there are some peculiarities about writing JSX in Reason which make life for beginners hard, especially when they come from JS and are used to write strings directly into a <div /> or any other React element which allows children. In Reason, the React.string method is needed everywhere where you want to render text in your app, so it makes sense to create a binding for it to a one-letter function.

You could do that by writing

let s = React.string;
Enter fullscreen mode Exit fullscreen mode

but you can utilize the compiler to optimize all unnecessary parts away, by using the external keyword:

/* Shorter replacement for React.string */
external s: string => React.element = "%identity";
Enter fullscreen mode Exit fullscreen mode

To compare the methods, check out this example in the ReasonML playground: ReasonML Try - 1.

As you can see, there is still a function s being created in the first example, whereas the second one is nowhere to be found. It's probably a very very small runtime improvement, but it gives a good idea of how the BuckleScript compiler works. And if you look at the sources, it is actually the same thing as the provided React.string from ReasonReact.

In either way, you probably should put the helper function in a utilities module which you can open everywhere you need those precious strings in react elements, like so:

open ReactUtils;

[@react.component]
let make = () => {
  <div>
    {s("Hello, dev.to!")}
  </div>
};
Enter fullscreen mode Exit fullscreen mode

I named the module ReactUtils, because it is specifically meant for working with React elements.

Keep your codebase free of magic!

Sometimes the type system is just too strict, and in a heterogenous software system there may exist types which are completely the same thing, only with different names. With the identity trick you can convert types when they would be actually the same, for instance Js.Dict.t(string) and Js.Json.t:

type apples = Js.Dict.t(string);
type oranges = Js.Json.t;

let (apples: apples) = Js.Dict.fromArray([|("taste", "sweet")|]);

let eatFruits = (fruits: Js.Json.t) => Js.log2("Eating fruits", fruits);

eatFruits(apples);
Enter fullscreen mode Exit fullscreen mode

As you can see, we have apples and oranges here. Both are fruits, as commonly known, but only oranges are actually also defined as Js.Json.t. Apples however are defined as Js.Dict.t(string) which makes the compiler throw an error (see ReasonML Try - 2).

The easiest way to make this code compile is to use Obj.magic. It basically switches the type checker off and lets the compiler do his job.

eatFruits(apples->Obj.magic);
Enter fullscreen mode Exit fullscreen mode

(ReasonML Try - 3)

Obj.magic is actually also implemented by using the identity trick:

external magic : 'a => 'b = "%identity";
Enter fullscreen mode Exit fullscreen mode

(see the source code).

But it is both more idiomatic (and less risky) to write a conversion function for the two specific types:

type fruits = Js.Json.t;
external applesToFruits: apples => fruits = "%identity";

eatFruits(apples->applesToFruits);
Enter fullscreen mode Exit fullscreen mode

This lets your code compile and still ensures that there is only one distinct type consumed by the function and not everything. It still may make sense to use Obj.magic sometimes, especially when you just want to create a quick prototype (see ReasonML Try - 4).

Pipes

Use the Pipes like Mario!

As many (functional) languages do, also Reason provides us with a special pipe operator (->), which essentially flips the code inside out, e.g. eatFruits(apple) becomes apple->eatFruits. At first it was hard for me to read and comprehend longer pipe chains, but I got used to it after some days of using them. Now they are one of the most indispensable features to me.

  • Keeps you from needing to find a name for in-between variables.
  • Keeps code tidy.
  • Especially useful with Belt (BuckleScript's standard library) and its Option module which you will encounter very often as we have no null or undefined here in Reason land.
  • When using pipe-first (->) with, say Array.mapit makes the code look pretty similar to Js, e.g.: [|1, 2, 3|]->Array.map(x => x * 2) which would be [1, 2, 3].map(x => x * 2) in plain JS.

But just compare the two examples below, first one without using pipes:

let vehicle = Option.flatMap(member.optionalId, id => vehicles->Map.String.get(id));
let vehicleName = Option.mapWithDefault(vehicle, "–", vehicle => vehicle.name);
s(vehicleName);
Enter fullscreen mode Exit fullscreen mode

vs.

member.optionalId
  ->Option.flatMap(id => vehicles->Map.String.get(id))
  ->Option.mapWithDefault("–", vehicle => vehicle.name)
  ->s
Enter fullscreen mode Exit fullscreen mode

Pipe parameter position paranoia!

I know there is some war going on between the last-pipers (mostly native Reason and OCaml developers) and the first-pipers (BuckleScript developers), but in BuckleScript, because you have JS as the target language, pipe-first is the way to go. The type inference also works better that way.

If you really want to pipe into a function which is not idiomatic to BS (i.e. the positional parameter is not the first one), you can use the _ character to substitute where the piped parameter should go:

"ReasonReact"->Js.String.includes("Reason", _);
Enter fullscreen mode Exit fullscreen mode

but rather use the pipe-first version of a function preferably, as long as it exists:

"ReasonReact"->Js.String2.includes("Reason");
Enter fullscreen mode Exit fullscreen mode

as mentioned earlier.

Labels to the rescue!

When you have a function like String.includes where multiple parameters have the same type, it would be much better to label them directly, otherwise you won't know which of the parameters is the source string, and which one is the search string:
string.includes

Even worse, if you use the wrong pipe (|>) and are unsure which parameter it takes, you can get confused easily. And the compiler cannot save you from that case, as the types are totally correct.

Here is a String.includes function binding with labels to keep you from guessing which one the positional parameter is:

module BetterString = {
  [@bs.send] external includes : (string, ~searchString: string) => bool = "includes";
};

"ReasonReact"->BetterString.includes(~searchString="Reason");
Enter fullscreen mode Exit fullscreen mode

(ReasonML Try - 5)

To be fair, we now need to type a bit more, but we gain some doubtlessness and do not have to check MDN to be sure. Also, when you ignore warnings or deactivate the corresponding compiler warning (it's number 6), you could still call the function without labelling the parameter. I would still advise you to not do that, though.

That's all for the first part of Reason(React) Best Practices, our next topic will be about BuckleScript's compiler configuration and Belt.

Top comments (9)

Collapse
 
yawaramin profile image
Yawar Amin

These are, somewhat, the forbidden fruits of Reason :-) Anyway, this is a good 'tricks of the trade' guide to the working practitioner. The only things I would add, are that the React string identity trick is exactly how the actual React.string function is defined, and that Obj.magic is basically a name given to the %identity trick itself. So e.g. you could do

let jsDictToJsObj = Obj.magic;
let test = jsDictToJsObj(Js.Dict.empty());
Enter fullscreen mode Exit fullscreen mode

And you'd get var test = {};

Collapse
 
fhammerschmidt profile image
Florian Hammerschmidt

Thank you for your input, I really appreciate it.
I updated the post.

Collapse
 
idkjs profile image
Alain

In your BetterString example, how do we know which includes function we are accessing? In the examples before that you call Js.String and Js.String2. Which is being used in BetterString?

Thanks for sharing this post. Very useful.

Collapse
 
fhammerschmidt profile image
Florian Hammerschmidt • Edited

If you look up [@bs.send] in the Bucklescript docs , you see that it can be used for object methods such as Array.prototype.map() or String.prototype.includes(). Thus, that is all you need for using the internal JS methods on all available JS prototypes.

Check out how it works in Reason Try!

Many thanks for your feedback!

Collapse
 
idkjs profile image
Alain

Danka, sir. Also missed this link github.com/moroshko/bs-blabla at the bottom of those docs. Thanks for sending me back there.

Collapse
 
idkjs profile image
Alain
external applesToFruits: apples => fruits = "%identity";
Enter fullscreen mode Exit fullscreen mode

throws: Error: Unbound type constructor fruits.

Works if you add type fruits = Js.Json.t; to file but not sure if this is what you were going for.

Collapse
 
fhammerschmidt profile image
Florian Hammerschmidt

You 're right. I forgot to add a type fruits = Js.Json.t;definition somewhere above, fixed it now. I updated the post section with some ReasonML Try examples, so that one easily can verify it for themselves.

Collapse
 
idkjs profile image
Alain

typo maybe? Should we change fruit to fruits here:

let eatFruits = (fruits: Js.Json.t) => Js.log2("Eating fruits", fruits);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fhammerschmidt profile image
Florian Hammerschmidt

Correct, I also updated this error now. Thanks for your help.