In this post, I'll show you how to write ReasonReact bindings for React.js components.

For all of you who don't know about ReasonML: ReasonML is syntactic sugar on top of the OCaml toolchain. ReasonML/OCaml can be compiled into optimized Javascript with Bucklescript. You can learn more about it by checking out this article by Dr. Axel Rauschmayer: What is ReasonML?

#The official way according to the docs

ReasonReact provides a great way to interop with React.js components using ReasonReact.wrapJsForReason . Here is an example from the ReasonReact docs:

[@ bs.module] external jsPersonalInformation : ReasonReact.reactClass = "./PersonalInformation.js" let make = (~name: string, ~age: option(int)=?, children) => ReasonReact.wrapJsForReason( ~reactClass= jsPersonalInformation, ~props={ "name" : name, "age" : Js.Nullable.from_opt(age) }, children )

Now we'll use the above component like this:

<PersonalInformation name = "Khoa Nguyen" age =Some(24) />

PersonalInformation.js should be called with these props:

{ name : "Khoa Nguyen" ; age: 24 ; }

Everything is working as expected. Now let's try with slightly different props:

<PersonalInformation name = "Khoa Nguyen" age =None />

PersonalInformation.js should be called with these props:

{ name : "Khoa Nguyen" ; age: undefined ; }

age now has an undefined value. It's not so bad in most cases, but it could problematic.

Here is how I implemented my PersonalInformation.js component:

import React from "react" ; const PersonalInformation = props => { let hasAge = props.hasOwnProperty( "age" ); if (hasAge) { return ( < p > My name is {props.name}. I'm {props.age} years old </ p > ); } return < p > My name is {props.name} </ p > ; }; export default PersonalInformation;

I've created a CodeSandbox here for you to play around with.

With the the first example ( ~age=Some(24) ), it renders My name is Khoa Nguyen. I'm 24 years old as expected.

With the second example ( ~age=None ), however, it renders My name is Khoa Nguyen. I'm years old.

What is going on?

Let's open the node repl and try it out:

❯ node > let firstExample = { name : "Khoa Nguyen" , age : 24 }; undefined > firstExample.hasOwnProperty( "age" ) true > let secondExample = { name : "Khoa Nguyen" , age : undefined }; undefined > secondExample.hasOwnProperty( "age" ) true > "age" in secondExample true

Ah. This totally makes sense. The age property has a value of undefined .

Quick note:

I know that the above code is not idiomatic React code. I can re-implement the React component like this to fix it:

<p> My name is {props.name}. {props.age && < span > I'm {props.age} years old </ span > } < /p>

This should work even with age = undefined , but the fact that I have to change the original component to write a binding isn't ideal. The pattern I use here ( props.hasOwnProperty("age") ) is common for switching between controlled/uncontrolled mode of a component.

#"The right way" of writing ReasonReact bindings

Now we've identified our problem, let's fix it.

We need to find a way for not defining age in props.

Lucky for us, Bucklescript provides a function called the Special Creation Function

The idea is simple:

[@bs.obj] external makeProps : (~name: string, ~age: int =?, unit) => _ = "" ; let props1 = makeProps(~ name = "Khoa Nguyen" , ~ age =24, ()); let props2 = makeProps(~ name = "Khoa Nguyen" , ());

Tips: You can try this on the playground at https://reasonml.github.io/en/try.html

This is the compiled code from Bucklescript:

var props1 = { name : "Khoa Nguyen" , age : 24 , }; var props2 = { name : "Khoa Nguyen" , };

As you can see, Bucklescript creates an object for us so no intermediate step is needed, and the age property is not in props2 .

With this knowledge, we can rewrite our binding like this:

[@ bs.module] external jsPersonalInformation : ReasonReact.reactClass = "./PersonalInformation.js" [@ bs.obj] external makeProps : (~name: string, ~age: int=?, unit) => _ = "" let make = (~name, ~age=?, children) => ReasonReact.wrapJsForReason( ~reactClass= jsPersonalInformation, ~props=makeProps(~name, ~age?, ()), children )

As you can see, I removed the type annotations in make and put them in makeProps . It's not necessary to annotate types everywhere with a great compiler. Now, our binding works as expected.

#Transform props

The above is a simple case. We sometimes need to apply some transformations to the props before passing them to React.js. A common case might be transforming between Js.boolean and ReasonML's boolean.

Here is a small snippet on how you can do that:

[@ bs.module] external jsPersonalInformation : ReasonReact.reactClass = "./PersonalInformation.js" [@ bs.obj] external makeProps : ( ~name: string, ~age: int=?, ~ showAge: Js.boolean=?, unit ) => _ = "" let make = (~name, ~age=?, showAge=?, children) => ReasonReact.wrapJsForReason( ~reactClass= jsPersonalInformation, ~props=makeProps( ~name, ~age?, ~ showAge=?Js.Option.map([@bs] (a => Js.Boolean.to_js_boolean(a)), showAge), () ), children )

You can use this to make your code more idiomatic ReasonML by replacing string enums with polymorphic variants:

[@bs.deriving jsConverter] type theme = [ | `dark | `light ]; [@bs.obj] external makeProps : ( ~name: string, ~age: int =?, ~theme: string =?, unit ) => _ = "" ; let make = (~name, ~ age =?, theme =?, children) => ReasonReact.wrapJsForReason( ~ reactClass =jsPersonalInformation, ~ props =makeProps( ~name, ~age?, ~ theme =?Js.Option.map([@bs] (a => themeToJs(a)), theme), () ), children );

I think this is enough for this blog post. If you want to see more example of the pattern on writing ReasonReact binding, checkout my Ant Design binding here: https://github.com/thangngoc89/bs-ant-design

This is my first time writing a blog post in English, so please let me know If I wrote anything that's incorrect.

Update 3/16/2018: If you're having troubles running the code in the example, you could try with this repository

You can reach me via Twitter(@thangngoc89) or @thangngoc89 on Discord channel.