Sam SlotskyEngineering @ Manifold

Building reason-react with parcel-bundler

ReasonML is a lot of things, including but not limited to:

  1. The cool new kid, fashionably late to the JavaScript static typing party

  2. The OCaml language with a JS-like syntax

  3. An awesome way to build type-safe React apps

To accomplish that last trick, there’s a library called reason-react, and the getting started guide shows two easy ways to build a working React application in a single command. Both of them use webpack and have different advantages; while the recommended approach is simple and lightweight, the alternative uses create-react-app to give you a fully featured development experience, including hot module replacement, small production builds, and a whole lot more. But to me, both of these solutions seem a little magical, so I’d like to demystify things a bit.

I think the best way to do that is to build a reason-react app from scratch, much like we did with React apps beforecreate-react-app blessed us with great defaults, but without the extra work of configuring webpack. My goal is to keep things lightweight like the preferred approach, and to add in hot module replacement. We’ll be using the Parcel bundler for this, and we’ll have to learn some basic Bucklescript FFI in order to accomplish the HMR. Let’s bundle up!


If you want to skip the guide and go straight to the code, check out the repo. Otherwise, read on!

Step 1: Get Buckled

We need to initialize a basic Bucklescript app first. You’ll need bs-platform for this, which has some system dependencies, like a c compiler and make. Once you have it installed, you can create an app:

1$ bsb -init sparcely-unreasonable -theme basic-reason

Open up your editor in the new directory and you should see something like this.

A Reason app generated with bsbA Reason app generated with bsb

There’s one file in our src/ folder, called This file calls the Js.log function which is a bit of interop that compiles to console.log. You can take my word for that, but instead, let’s prove it by building the project. Go to the console and run:

1$ yarn build

By the way, I’m using yarn for this. Feel free to use npm commands instead!

Now back in the code editor, notice that there’s a file with a console.log statement corresponding to the Js.log statement. This is the output of compiling

The result of compiling Reason codeThe result of compiling Reason code

Since we now have a .js file, we can run it with node.js, or from within a browser. We want the latter, and we want to use parcel-bundler to do it, which starts with an installation step.

1$ yarn add --dev parcel-bundler

A minimal html file in the project root that includes the entry point for our JavaScript is all we need when we use parcel:

3 <div id="root"></div>
4 <script src="./src/"></script>

We can now bundle and serve our application:

1yarn parcel index.html

One neat thing about parcel is that it doesn’t require configuration. Webpack would require you to declare an entry point and a bundle name in a config file, and the index.html file would have a tag that references the name of your bundle. With parcel, the tag refers to the entry point, so we can skip the mapping step.

In my case, parcel bundled my application and started a server on port 1234. When I navigate there I can see the message in my console. Step 1 complete!

Message received: a minimal reason app built with parcelMessage received: a minimal reason app built with parcel

Step 2: Add a Dash of React

Next we’ll go through some minimal steps that will allow us to use React. As you might expect, we need the react and react-dom libraries to do React development on the web, but with ReasonML in the picture, we have one more dependency:

1$ yarn add react react-dom reason-react

The reason-react library forms the bridge from ReasonML to React. It contains Bucklescript bindings for both react and react-dom. This package is made up of interop functions and abstractions that make it possible to write React code in OCaml! But before we can use it, we have to make some updates to our bsconfig.json.

Here’s where quite a bit of the magic happens. For guidance on what to do, I looked at a configuration file from a project that was made with reason-scripts, and I saw a couple key differences:

1 "bs-dependencies": ["reason-react"],
2 "reason": {
3 "react-jsx": 2
4 },
5 "bsc-flags": ["-bs-super-errors"],
6 "package-specs": {
7 "module": "es6",
8 "in-source": true
9 },

I suggest reading about how this config file works, but these don’t look too scary. Declaring bs-dependencies tells Bucklescript to load Reason code from the given node modules. The react-jsx setting gives us JSX syntax (yay!), we’re using es6 modules rather than common, and we have some option that presumably has some effect on error output. Here’s the whole thing:

Embedded content:

Now we can write some React code! I’ll create a new file src/ and write perhaps the simplest possible React component:

Embedded content:

This is how reason-react does stateless components. You have to define a make function, which returns a record that extends an instance of ReasonReact.statelessComponent and adds a render function returning some JSX. The next step is to render our component to the DOM. That’s as easy as replacing the contents of with this:

1ReactDOMRe.renderToElementWithId(<App />, "root");

Maybe you’ve noticed that I haven’t been importing anything. That’s because I don’t have to! By referencing App, I’m telling Bucklescript to look in my code path for a file named or This file becomes a module with its top level variables accessible, allowing us to render the App component.

Run yarn build to recompile the Reason code, then reload your browser window and have a look. Success!

We did it! A reason-react app built with parcelWe did it! A reason-react app built with parcel

Now to crack one last nut.

Step 3: Hot Reloading

Raise your hand if you like restarting the server when you make a change? That’s what I thought. Right now we are manually running our build process in two steps. The first compiles ReasonML to JS, and the second prepares the bundle and starts the server. We’re executing each of these by hand. Can we make this better? I think so.

For starters, I might not have mentioned that bsb can also run in watch mode. Let’s try this out. Kill your parcel server for now and let’s compile our ReasonML code again, but with a different command:

1$ yarn start

Your package.json should reveal that this task just adds the -w flag to the Bucklescript compilation task. This puts the compiler in watch mode so that your .re files will recompile to .js every time you make a change. Let’s see what this looks like in practice. Go back into and introduce an error by changing to , for example. You should see something like this in your terminal:

Some error output from bsbSome error output from bsb

Go ahead and change it back to normal, and watch the error disappear. Now that we know this is possible, perhaps you’ve connected the dots: if we add hot module replacement to parcel and make sure that bsb is in watch mode while our server is running, then our browser should pick up our changes automatically. This is where things get interesting.

To do HMR, we need to define some external functions with Bucklescript’s foreign function interface. This is how we declare types for JavaScript code that we need to interface with.

Embedded content:

I’m free to use whatever names make sense to me for the types of objects that are returned by this interface. Lines 3–5 declare ReasonML functions that compile down to calls into global parcel variables, e.g. Notice the Js.nullable stuff. We need this to indicate that a type might not have a value, because we can’t access null references in ReasonML so it needs special handling. A snippet shows this in action:

1switch (Js.Nullable.toOption(parcelModule |> hot)) {
2 | Some(h) => h |> accept()
3 | _ => Js.log("We are not hot")

Here we’re performing a switch over our object to see if it has a value. We have to wrap this in Js.Nullable.toOption to convert it to an option type, which is you you handle nullable data in ReasonML. If has a value, then it matches the first case and we call hot.accept(). Otherwise it prints a silly message. Here’s our in its entirety.

Embedded content:

This is enough for HMR to work, but we need bsb and parcel to be running at the same time, and it would be best if we could start them both with a single task. If you’re a linux user, you might be tempted to try something like this:

1$ yarn start & yarn parcel index.html

It will work, but with a caveat: if you introduce an error in your ReasonML code, you won’t see the compiler output in the console. Fortunately, there’s a better way through npm-run-all. Install that first:

1$ yarn add --dev npm-run-all

Now let’s modify our package.json a bit.

1 "scripts": {
2 "build": "bsb -make-world",
3 "dev": "npm-run-all --parallel start serve",
4 "start": "bsb -make-world -w",
5 "serve": "yarn parcel index.html",
6 "clean": "bsb -clean-world"
7 },

Now our dev task will run bsb and parcel in parallel, and we’ll still see our error output, so we can kill our separate processes and just run a single task:

1$ yarn dev

At this point, I can open a browser window and connect to my app, and if I make a legal change, I’ll see it reflected immediately on the screen. And if my change causes an error instead, I’ll see the output in my terminal window.

bsb & parcel running in parallel, with all output visible, via npm-run-allbsb & parcel running in parallel, with all output visible, via npm-run-all


I now have a pretty solid developer experience for my reason-react app, and my project only has a couple of code files and no mystery whatsoever. And we were able to implement hot module replacement with just a few lines of interop code.

The magic never died!

And yet there are certainly more features that we could still demystify. In particular, the error reporting in reason-scripts adds the error output from the Bucklescript compiler to the error overly from create-react-app. This is a highly useful feature that would be well worth adding, but it’s a battle for another time, so we’ll have to remain under at least one spell for today.

Further Reading

Stratus Background

Sign up for the Stratus Update newsletter

With our monthly newsletter, we’ll keep you up to date with a curated selection of the latest cloud services, projects and best practices.
Click here to read the latest issue.