Introduction

(Note: There is a more recent version of this article)

Most of my Rescript series so far has been about combining Rescript with React using create-react-app. Now that I understand Rescript better, I’m not so sure create-react-app is a good fit for it.

Mostly because it’s slow. Rescript compiles JSX natively, so we shouldn’t need slow and hard-to-configure webpack. Most of create-react-app is about hiding the configuration of webpack from the end-user. I appreciate that. I’ve wasted more hours on webpack configuration than I care to count. But not having webpack is even better than hiding it!

So in this article, I explore setting up a minimal rescript-react project that uses the ultra-high speed esbuild as a bundler.

Patreon

This article is part of a series on Rescript programming, though it is another stand-alone article with no hard dependencies on my earlier tutorials.

This series takes a lot of time and dedication to write and maintain. The main thing that has kept me invested in this writing this series is support from my Patrons.

Other ways to show support include sharing the articles on social media, commenting on them here, or a quick thank you on the Rescript forum.

With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.

You do not need to have read any of my earlier articles to understand this one, as I’ll be starting a new project from scratch. That said, my musings probably assume you have certain amount of knowledge about Rescript already, and those earlier articles are a great place to acquire that!

Let’s begin

This is a brand new Rescript project, so I’m going to go through the flow from the beginning. Rescript has changed their flow a bit since I first started working with it, and it’s much simpler now.

Start by cloning the Rescript template repo:

git clone git@github.com:rescript-lang/rescript-project-template.git fast-bare-rescript-react
cd fast-bare-rescript-react

If you are creating a “real” app, you may prefer to click the Use This Template button on the project template repo. This will create a new repository in github under your username.

This is what I did to create my repository. I make commits to that repository roughly following the steps in this article if you want to follow along.

Initial configuration

I’ve been coding Rescript for a while now, so I have a pretty good idea as to how I want it configured. Rescript’s defaults are quite a bit nicer than they were when I started, so there isn’t much to change.

In package.json, I made a few minor changes:

  • I changed the name to "fast-bare-rescript-react"
  • I added "type": "module"
  • I renamed the "start" script to "build:watch" because I will later want start to reflect a devserver instead.
  • I added a "build:deps": "rescript build -with-deps" script.

The current state of my package.json is as follows:

{
  "name": "fast-bare-rescript-react",
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "build": "rescript",
    "build:deps": "rescript build -with-deps",
    "build:watch": "rescript build -w",
    "clean": "rescript clean -with-deps"
  },
  "keywords": [
    "rescript"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "rescript": "*"
  }
}

I also made some similar changes to bsconfig.json, the Rescript build configuration file:

  • I changed the name to "fast-bare-rescript-react"
  • I changed the package-specs->module to es6-global so modern browsers can import files directly.
  • I changed the suffix to “.mjs” because I find that works best with es6-global.
  • I added "reason": {"react-jsx": 3} to support JSX bindings for Rescript-react. This essentially tells Rescript to compile </>-style tags to a certain set of function calls around React’s createElement function.

Here’s my complete bsconfig.json:

{
  "name": "fast-bare-rescript-react",
  "version": "0.0.1",
  "sources": {
    "dir": "src",
    "subdirs": true
  },
  "package-specs": {
    "module": "es6-global",
    "in-source": true
  },
  "suffix": ".mjs",
  "bs-dependencies": [],
  "warnings": {
    "error": "+101"
  },
  "reason": { "react-jsx": 3 }
}

Add a couple dependencies

My goal is to make this is dead simple as I possibly can, which means minimal dependencies. I’m starting with the following:

yarn add react react-dom rescript-react
yarn add --dev vite

Rescript-react is obviously needed for defining the react components. vite gives us a super-fast devserver built on top of the (also super-fast) esbuild system.

One of the minor warts (there is talk of fixing them) in Rescript development is that you have to maintain two dependency arrays. So you’ll need to change the bs-dependencies as folows:

"bs-dependencies": ["@rescript/react"]

Test the build

Let’s make sure the build config is correct. Run yarn build:watch in one terminal. It should compile successfully, probably in under 10 ms.

In another terminal, run node src/Demo.mjs. If it spits out Hello World, you’re good to go.

First React Component

Open the Demo.res file that came with the project template. Let’s change it to a react component:

@react.component
let make = () => {
  <div> {React.string("Hello World")} </div>
}

The terminal running yarn build:watch should show it recompiling successfully. If it didn’t, you probably missed one of the dependency updates above.

You might find it instructive to open the Demo.mjs file that Rescript built for you:

// Generated by ReScript, PLEASE EDIT WITH CARE

import * as React from "react";

function Demo(Props) {
  return React.createElement("div", undefined, "Hello World");
}

var make = Demo;

export {
  make ,

}
/* react Not a pure module */

As you can see, Rescript outputs highly readable Javascript. The Demo module has been compiled down to a function component that accepts a Props model, (which, in this case, is empty, because our Demo component doesn’t have any props). The Jsx compiles cleanly down to a React.createElement call.

Nothing terribly interesting going on under the hood. That’s always a good sign; I like code to be minimal and understandable (and tooling to be fast)!

Rendering the Component

Rescript-react comes with a ReactDOM module that wraps the Javascript module of the same name. Create a new Index.res to render our Demo component as follows:

exception NoRoot

switch ReactDOM.querySelector("#root_react_element") {
| Some(root) => ReactDOM.render(<Demo />, root)
| None => raise(NoRoot)
}

querySelector can return None if the element doesn’t exist, and I chose to handle that by raising an exception.

If the build system has happily spit out a src/Index.mjs file, you’re ready to set up a basic HTML file to render it. I’m going for dead-simple for now, so I didn’t create a very good HTML file (put it in the top level directory, alongside src):

<!DOCTYPE html>

<html>
  <head>
    <title>Fast Bare Rescript React</title>
  </head>
  <body>
    <div id="root_react_element"></div>
    <script type="module" src="./src/Index.mjs"></script>
  </body>
</html>

The type="module" bit is important because we’ve configured Rescript to spit out es6-global style imports.

Configuring Vite

Vite is a super fast dev server that has all the features you expect from a modern build system, without all the slowness and nasty configuration. We already installed it in the “Dependencies” step above, so all we need to do to enable it is add the start script back to package.json (See, I told you I wanted to use it for a devserver!):

{
  ...
  "scripts": {
    ...
    "start": "vite"
  }
}

Now you can run yarn start and visit http://localhost:3000/index.html to see your app in action. It should say…. “Hello World”.

However, if you change the text in Demo.res to, say “Demo File” instead of “Hello World”, you’ll notice that it doesn’t refresh automatically like you might be used to from create-react-app development. Don’t worry, Vite has us covered.

First, add a new dependency on react-refresh, an official ViteJS plugin:

yarn add --dev @vitejs/plugin-react-refresh

Now we have to add a tiny bit of configuration to set up this plugin, inside a file called vite.config.js:

import { defineConfig } from "vite";
import reactRefresh from "@vitejs/plugin-react-refresh";

export default defineConfig({
  plugins: [reactRefresh({ include: "**/*.mjs", exclude: "/node_modules/" })],
});

Kill and restart your dev server and try editing Demo.res a few times. Notice how fast the page reloads in your browser.

Hack!

Now you can do whatever React development suits your fancy. You may find my other Rescript articles useful, along with the Rescript React documentation.

Build!

After a while you’ll have an awesome website that you will want to deploy to the world on your favourite static site host (mine is Netlify). You can add one simple command to your package.json to make a release distribution:

{
  ...
  "scripts": {
    ...
    "release": "vite build"
  }
}

Now if you run yarn release, you will have a fully productionized release build in your dist/ folder in a matter of seconds. You can test it using http-server:

npx http-server

and visit localhost:8080.

Conclusion

This ended up being a lot easier than I expected. Vite seems to be “fire and forget”, and fits in well with Rescript’s emphasis on speed. It takes about 40ms to compile and another 40 for Vite to reload the page during development for me. That’s approximately the amount of time it takes for my eyes to scan from my code editor to my browser (across three monitors…).

I doubt I’ll ever use create-react-app again, and almost regret writing my initial series around it. If this series ever turns into a book (reminder, my Patreon account is the best way to motivate my editor), I’ll start from this simple setup instead of spending so many cycles on create react app.