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;
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";
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>
};
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);
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);
Obj.magic
is actually also implemented by using the identity trick:
external magic : 'a => 'b = "%identity";
(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);
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 itsOption
module which you will encounter very often as we have nonull
orundefined
here in Reason land. - When using pipe-first (
->
) with, sayArray.map
it 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);
vs.
member.optionalId
->Option.flatMap(id => vehicles->Map.String.get(id))
->Option.mapWithDefault("โ", vehicle => vehicle.name)
->s
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", _);
but rather use the pipe-first version of a function preferably, as long as it exists:
"ReasonReact"->Js.String2.includes("Reason");
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:
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");
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)
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 actualReact.string
function is defined, and thatObj.magic
is basically a name given to the%identity
trick itself. So e.g. you could doAnd you'd get
var test = {};
Thank you for your input, I really appreciate it.
I updated the post.
In your
BetterString
example, how do we know whichincludes
function we are accessing? In the examples before that you callJs.String
andJs.String2
. Which is being used inBetterString
?Thanks for sharing this post. Very useful.
If you look up
[@bs.send]
in the Bucklescript docs , you see that it can be used for object methods such asArray.prototype.map()
orString.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!
Danka, sir. Also missed this link github.com/moroshko/bs-blabla at the bottom of those docs. Thanks for sending me back there.
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.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.typo maybe? Should we change
fruit
tofruits
here:Correct, I also updated this error now. Thanks for your help.