Setting Up A Rescript Create-React-App From Scratch
Introduction
I recently discovered the Rescript programming language, and have become very excited about it. There aren’t a lot of tutorials on it yet. I collect some links below, but I decided it was worth writing my own.
This is the first in a long-multipart series on Rescript. Will update with links to other parts when I write them.
This part is about migrating the create-react-app boilerplate to typescript later articles, I’ll turn it into a complete app.
Patreon
This series takes a lot of time to write and maintain. I’ve started it while I have some free time between jobs, but I’ll probably need some extra motivation to keep working on it once I’m busy with work again.
I’m not one for begging for money on the Internet, but if you’re interested in providing some of that motivation, I’ve set up a Patreon account. I thank you for your support.
As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.
Other articles in this 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.
Introducing Rescript
Rescript is a functional language derived from
Reason. Reason itself is derived from a merging
of OCAML and Javascript. Rescript is not actualy a new language; it’s a
rebranding of Bucklescript. Bucklscript was originally just a toolchain to
compile OCAML/Reason to Javascript. But now it is its own language. You’ll
still see references to bucklescript in the ecosystem, as well as the bs
prefix and files that end in .bs.js
.
Rescript has a very comfortable Javascript-like syntax and outstanding JS interop. I really like the syntax and design decisions I’ve seen. Coding in Rescript, compared to Typscript feels as much safer than Typescript does compared to Javascript.
I have a feeling this will be the Next Big Thing. It feels that much safer than typescript and the compiler is much more helpful. It is also blazing fast. You will not believe how much quality of life difference it makes to have things compile this fast!
I also like how little config is required, though it is more than it should be. I have wasted way too much time trying to set up linting in both JS and TS! Like Typescript, the output is raw JS, so you can use your usual JS tooling.
Some links
- Rescript website
- Rescript-react
- ReasonML
- Rescript docs
- The only tutorial I could find
- Real World example app
- Code for this tutorial
- The commit messages loosely relate to specific sections in this tutorial.
Prior knowledge
I am assuming you already know React and its underlying technologies. I make references and comparisons to Typescript, but you don’t need to know it. You should have passing knowledge of functional programming principles. If you know redux and an immutable library, that’s sufficient. You should have some knowledge of static type analysis. This could be gradual typing such as Typescript, Flow, Python type hints, or static typing such as Rust, Go, Java, or C No need to know Reason or OCAML (I don’t!)
Technologies
I’m keeping this light, so just a couple libraries:
- Rescript of course
- Rescript-react provides React bindings
Let’s begin
First, install the Rescript compiler:
npm install -g bs-platform
- Run
bsb -version
to make sure it’s installed
For what it’s worth, my version is 9.0.0
at time of writing.
You’ll probably want to install an editor language extension for rescript:
I should note that editing Rescript is a dream because the lint errors show up instantaneously. The compiler is that fast. It’s amazing. I actually had to install a faster terminal emulator so neovim could keep up!
A couple wild geese
To save you wasting some time, here are a couple dead ends I went down:
First, don’t use ReasonReact
. Use rescript-react
. It’s more or less just a
renaming of ReasonReact, but it’s been and will continue to be, updated to work
with the newer Rescript ecosystem.
Second, there is a rescript template
for create-react-app
You can install it with npx create-react-app --template=rescript rescript-react-intro
It does not seem to be a very popular repository, yet and hasn’t been updated
in four months. I compared the template to the default create-react-app template
at time of writing, they are virtually identical .
Honestly, the template would probably work for you, but for my edification (and yours), I decided to start with a default create-react-app install. I’m keeping this repo open beside me for reference, and it’s close to what ’ll end up with. In fact, you could clone the branch linked at the end of this article to have a good starting point for your own Rescript-react project.
Begin (for real this time)
Just create a vanilla react app with the default Javascript template
npx create-react-app rescript-react-intro
cd !$
Now add a few dependencies:
npm install --save-dev bs-platform gentype
npm install --save @rescript/react # --force
bs-platform
needs to be installed locally as well as globallygentype
probably isn’t needed for this project. It’s used for Typscript/Flow interop but it’s kind of standard to have it installed.- rescript-react is a very recently released port of the older ReasonReact
project. I found that it didn’t want to install with the 9.0.0 version of
Rescript, so I
--force
d it. This will probably bite me in the end, but I imagine by the time you read this, rescript-react will have released a fix.
Configure Rescript
Rescript needs a bit more configuration before we can use it. This is a sore
point for me. I’ve been scarred by too many babel/webpack/eslint/tsconfig nightmares!
It should not take half an hour of reading documentation to get config set up for hello world
!
I think the Rescript team is working on improving the tooling. There’s quite a bit of legacy stuff going on from the Reason/bucklescript days, and one of the most exciting things about the language is how new it is and how quickly it is evolving.
For what it’s worth, I based my config on:
- The one spit out by
bsb -init react-hooks
- The one spit out by
bsb -init reason-react
- The one spit out by
create-react-app --template=rescript
- The output of
bsc -help
- This tutorial
- The official docs
You’ll need a new bsconfig.json
file in the main directory, beside package.json:
{
"$schema": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json",
"name": "rescript-react-intro",
"reason": {
"react-jsx": 3
},
"sources": {
"dir": "src",
"subdirs": true
},
"bsc-flags": ["-bs-super-errors"],
"refmt": 3,
"package-specs": [
{
"module": "es6",
"in-source": true
}
],
"suffix": ".bs.js",
"namespace": true,
"gentypeconfig": {
"language": "typescript"
},
"bs-dependencies": ["@rescript/react"]
}
This whole file is basically an itemized list of complaints. Note: I love Rescript and I think they will fix this experience soon. This is the only time I’ll complain about the language in the whole series.
$schema
is needed to keep my editor from complaining. The plugin is looking
for a non-existent schema by default. This will probably be fixed in Rescript
9.0. - probably hasn’t been updated since bucklescript got renamed to rescript
The two 3
s are suspect. They indicate the version of jsx
and the rescript formatter to use. They don’t default to 3
for backwards compatibility,
but they are the preferred setting.
The -bs-super-errors
flag sounds great in the docs: “Better error message
combined with other tools” so I don’t know why it isn’t on by default.
The .bs.js
suffix is “recommended” in the docs (to help the compiler drop
unused files), but the compiler outputs js by default. Recommendations should
be defaults!
It’s unclear what all the different subconfiguration categories mean What’s the difference between a flag, a package-spec, and a source?
And last, but not least, what the hell is all this bs
? Rescript is a recent
rebranding, but hopefully this will change soon, too.
Anyway, at the time of writing, the above config should serve you fine. But I have fears that when I start my next rescript project I’ll have to do all this research all over again. It’s not the kind of thing you do often enough to memorize.
For convenience, make a quick addition to your package.json
to make it easier
to build and run rescript projects.
"start:res": "bsb -make-world -w",
You can now run this in one terminal and npm start
in the other. Rescript
will build your files so fast that they’ll be ready to go by the time
the create-react-app script is ready to go.
Note: I actually don’t run this command. My editor builds the files for me so it can provide linting. So I don’t need to run a second builder.
Port create-react-app’s JS to rescript the easy way
Rescript might be building, but it doesn’t have anything to build yet!
There are five .js
files in the src directory:
App.js
App.test.js
index.js
reportWebvitals.js
setupTests.js
Let’s start by pseudo-porting two of them. Rename the App.js
and index.js
files to have the .res
extension:
git mv src/index.js src/Index.res
git mv src/App.js src/App.res
Then open both files and surround their contents with %%raw(...)
For example, Index.res
will now look like this, but do the same with App.res
:
%%raw(`
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
`)
If you run npm start
now, you’ll get an error message: “Could not find
required file index.js”
This is because webpack is configured by default to access index.js
- But the
output of compiling Index.res
is Index.bs.js
. I configured Rescript to put
it in the same directory as the other sources, so it won’t be too hard to find.
But rather than messing with the configuration, just add a new index.js
file
with one line:
import "./Index.bs";
This is the approach recommended by rescript. Check in your JS sources so
reviewers can see how the compiled output changes. Rescript compiles to very
readable Javascript (it’s a design goal). In fact, take a look at the
Index.bs.js
and App.bs.js
files in the src
folder.
For now, it’s just the raw JS from the .res files. Because we’re importing
Index.res
from index.js
, create-react-app is just looking at Javascript. It
doesn’t even know rescript exists.
npm start:res
should now compile successfully, but if you load it in the browser
there will be an import error. Let’s fix it.
Edit Index.res
to change the line import App from './App'
to
import App from './App.bs'
. As soon as you save, the file will compile to
./Index.bs.js
In milliseconds!
- Refresh the browser window and you should see the default react app again
Porting App.res (for real this time)
The two .res
files are technically valid rescript: %%raw
is a legitimate
rescript command. But, of course, it loses all the purported type safety of
rescript. It’s a perfectly reasonable choice if you’re in a hurry, but we can
do better. Rescript’s recommended porting strategy is to leave some stuff in
%%raw and migrate the stuff that has an obvious rescript implementation first.
Check the JS output and repeat.
Leave the two import statements in %%raw
since I’m Not sure how to import CSS
or svg from rescript yet. But it’s easy to migrate a function definition.
Change function App() {}
to let make = () => {}
.
make
is a Rescript convention for what you might call a “constructor” in rescript.
The React-rescript macros require that we name it thus. We use “fat arrow”
syntax because that is how a function is
defined
in rescript.
Also add a @react.component
decorator before the function definition.
This decorator instructs react-reason to reformat the function a bit.
Finally, remove the return
keyword. In rescript, the last line of the
function is automatically returned.
The jsx itself can stay pretty much identical except for a couple minor changes:
First, you need to explicitly tell it to insert React strings for text.
This makes it a bit more verbose, but it guarantees sound compile-time type safety.
You have to (get to) specify exactly what the compiler should expect.
So you’ll never pass an array of elements where a string is expected
or, worse, undefined
.
So, for example, change Edit <code>src/App.js</code> and save to reload.
to:
{React.string("Edit ")}
<code>{React.string("src/App.res")}</code>
{React.string(" and save to reload.")}
Similarly, change Learn React
to {React.string("Learn React")}
You’ll also need to change <img src={logo}...
to <img src={%raw("logo")}
since Rescript doesn`t know about the existence of the “logo” (the import
itself was defined in raw JS).
Lastly, remove export default App
. Rescript doesn’t have the concept of
exports. By default, every type in a file is pubilcly accessible, although
there are ways to enforce an interface.
This is how the file looks now:
%%raw(`
import logo from './logo.svg';
import './App.css';
`)
@react.component
let make = () => {
<div className="App">
<header className="App-header">
<img src={%raw("logo")} className="App-logo" alt="logo" />
<p>
{React.string("Edit ")}
<code> {React.string("src/App.js")} </code>
{React.string(" and save to reload.")}
</p>
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
{React.string("Learn React")}
</a>
</header>
</div>
}
This isn’t running yet due to an import error in Index.res
, but it compiles correctly.
Pedantically removing raw JS
To be pedantic, I want to remove the remaining %raw
calls. However, the CSS
one seems to be standard so I’ll leave it.
I have a later article queued up to tackle styling. Importing the svg can be
done using:
@module("./logo.svg") external logo: string = "default"
Don’t forget to remove the import from inside %%raw
.
Finally, revert the %raw("logo")
change we just made to logo
in the <img src../>
Here is the final file:
%%raw(`import './App.css';`)
@module("./logo.svg") external logo: string = "default"
@react.component
let make = () => {
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
{React.string("Edit ")}
<code> {React.string("src/App.js")} </code>
{React.string(" and save to reload.")}
</p>
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
{React.string("Learn React")}
</a>
</header>
</div>
}
It’s educational to take a look at the output App.bs.js
file. It’s shockingly
readable, though not as similar to hand-written JS as I expected based on the
hype. The worst part is that the JSX has been translated to their relevant
function calls. This is necessary, of course, since JSX itself is not valid
Javascript!
The most notable difference is that it is exporting make
as a named export
instead of a default - This is breaking Index.res
which we’ll port next
Fixing a couple bugs in Index.res
I did App.res
first because rescript recommends porting leaf JS files before
their parents. In other words, files that don’t import any JS code that you
wrote should be ported before files that call them.
If you run npm start
now, you will get an import error:
'./App.bs does not contain a default export'
This is easily fixed in the %raw JS in Index.res
. Just change
import App from './App.bs' to 'import {make as App} from './App.bs
.
If you run start:cra
now, everything should load and look like the default app.
But we aren’t done! There is still a big block of %%raw
in Index.res
.
Before we address that, though, let’s fix a subtle bug that exists in virtually every react application.
document.getElementById('root')
might return undefined
if that element
doesn’t exist. To illustrate, go ahead and change 'root'
to 'doesnotexist'
.
Your react app browser window will show an error.
Good news, though: this is a bug that cannot happen in rescript! But before we
go there, let’s fix it in the %raw
js code:
const root = document.getElementById("doesnotexist");
if (root) {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
root
);
}
Now a blank page will load because there is no doesnotexist
element. That’s
much better than an error!
Porting Index.js to proper Rescript code
Now let’s start teasing out some of that raw Javascript. The reportWebVitals
import seems simple and obvious, so let’s change it first.
Split the %%raw
block in two:
- one for the imports
- one for the rest of the block
Then remove the reportWebVitals
import and add a
@module("./reportWebVitals") external reportWebVitals: 'whatever = "default"
between the two %%raw
blocks.
These interop exports to JS are long and a bit confusing so let’s break it down:
@module("./reportWebVitals") external
is the syntax for binding a Rescript name to a javascript valuereportWebVitals:
is the name that it is being bound to'whatever
is the type it is getting bound to. In this case, we aren’t bothering to tell rescript anything about the type.'whatever
is a generic type. This obviously isn’t very type safe so we will revisit it later= "default"
is the “name of the js value you are referring to”, in the words of the docs. In this case, the default export of the module but it could be any exported value in the module.
If you save the file now, it will compile and load correctly. If it doesn’t,
you may have forgotten to change the doesnotexist
back to root
first.
Also move the reportWebVitals();
line outside of the %%raw
block. You can
lose the semicolon since Rescript doesn’t need them. The function call syntax
is the same for JS and Rescript, so the line can be otherwise unmodified.
Now let’s replace the rest of the lines in %%raw
code blocks. Start by
removing all of the raw imports except import './index.css'
Rescript is able
to automatically import ReactDOM
and App
based on the filenames.
Then replace the const root =
declaration with
let rootQuery = ReactDOM.querySelector("#root")
In Rescript, let
is immutable, so it does the same thing as const
in JS.
The truth is, you rarely want to mutate values in functional languages.
This is great because “Immutable by default” maps very well to redux-style
development. I love the boilerplate you’ll see in the actions and reducers part
of this series (spoiler alert: there is no boilerplate).
ReactDOM
is now using the react-reason version of ReactDOM
instead of the
JS import. It is available in your namespace because you put reason-react
in
both your bsconfig.json
and package.json
.
querySelector
is a ReactDOM function. I named the result rootQuery
to
indicate that it returns an Option
, not an element.
As with all jquery-style selector mini-languages, the #
in the selector
indicates an ID selection.
Now let’s see some Rescript magic. Replace the entire if
statement with:
switch rootQuery {
| None => ()
| Some(root) => ReactDOM.render(<App />, root)
}
Unlike in Javascript, switch
demands that you cover all options in the
pattern. In this case, because an Option
was returned, the two possible
values are None
and Some(element)
As an exercise, try removing the None
line and check the warning output.
The |
symbol indicates one possible pattern in the switch. I consistently
forget this symbol, probably because the similar construct in Rust does it
slightly differently and that’s what I’m more familiar with.
Some(root)
is simultaneously destructuring the option and binding a value to
root
. We can then pass that value, which is now guaranteed to exist, into the
ReactDOM.render
method.
As one last change, let’s narrow the scope on the 'whatever
type for
reportWebVitals
. reportWebVitals
is a function that accepts no arguments
and returns nothing. In Rescript, “Nothing” is represented in Rescript as
unit
.
Note that unit
is not the same as null/undefined. It is not a value you
can assign or compare to; it is just a marker indicating that there is nothing
being returned, or that a function accepts no arguments.
Since this function has neither arguments nor return value, the type signature is unit => unit
.
So change the rootWebVitals
external line to
@module("./reportWebVitals") external reportWebVitals: unit => unit = "default"
This is more exact than 'whatever
, and is safer. Anywhere in your rescript
code that calls this function will be checked to ensure it matches this type.
However, it is still a potetial source of errors; it is possible the upstream
This is inevitable when working with Javascript. JS simply isn’t as safe as Rescript.
But all your new Rescript code has type safety guarantees. So write lots of Rescript.
To wrap it up, here’s the final Index.res
file:
%%raw(`import './index.css';`)
`@module("./reportWebVitals") external reportWebVitals: unit => unit = "default"`
let rootQuery = ReactDOM.querySelector("#reactroot")
switch rootQuery {
| None => ()
| Some(root) => ReactDOM.render(<App />, root)
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
If, in the future, you are looking for a boilerplate to start your CRA Rescript project from you can this branch, which contains the state of this article.
Conclusion
So far in my explorations, I love this language. It is much more concise than Javascript and it compiles so much faster than Typescript. I love how it allows my editor to lint and reformats instantly. I never noticed how irritating that delay was until I no longer had to deal with it!
Notice how much more concise Rescript is and yet, it’s also soundly type-checked!
This is just the beginning. Please see my other articles, and there are many more to come!