Fast, Bare, Rescript React
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.
Other articles in series
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
toes6-global
so modern browsers can import files directly. - I changed the
suffix
to “.mjs” because I find that works best withes6-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’screateElement
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.