Converting a JavaScript React app to a ReScript React app.
Tags: rescript, react, webdev, typescript
Published: 2023-09-27 18:46:47

[ReScript](https://rescript-lang.org/) is "Fast, Simple, Fully Typed JavaScript from the Future". Let's take a look at how we can add it to an existing React project.

## Our project
[This simple counter app](https://github.com/arnab-datta/counter-app) is one of the examples you can find in the React docs.

## How does ReScript work with JavaScript?
ReScript can be dropped into an existing JS app and is a part of the JS toolchain and ecosystem. It doesn't have a different package manager so you can use NPM, Yarn, or PNPM ([even Bun](https://dev.to/jderochervlk/promises-async-and-await-in-rescript-with-bun-2840)!).

You just install it with `npm i rescript`.

## Getting started
We'll clone the repo, open up VSCode, and install the [ReScript extension](https://rescript-lang.org/docs/manual/latest/editor-plugins) so we have syntax highlighting and code completion.

Now we can install our ReScript dependencies:
```shell
npm i rescript @rescript/react @rescript/core
```

- [rescript](https://www.npmjs.com/package/rescript): The language compiler
- [@rescript/react](https://github.com/rescript-lang/rescript-react): ReScript bindings to React and support for JSX
- [@rescript/core](https://github.com/rescript-association/rescript-core): The ReScript standard library

> Note: this repo is a bit older and still uses React 16. We'll need to update this to React 18 so we can use the latest version of `@rescript/react`. We could make it work with React 16, but I think upgrading can be part of our refactoring.

Set up our ReScript config file, `bsconfig.json`.

```json
{
    "name": "rescript-counter-app",
    "sources": [
        {
            "dir": "src",
            "subdirs": true
        }
    ],
    "package-specs": [
        {
            "module": "es6",
            "in-source": true
        }
    ],
    "suffix": ".bs.js",
    "bs-dependencies": [
        "@rescript/core",
        "@rescript/react"
    ],
    "bsc-flags": [
        "-open RescriptCore"
    ],
    "jsx": {
        "version": 4,
        "mode": "automatic"
    }
}
```

> Note: ReScript used to be known as [Bucklescript](https://rescript-lang.org/blog/bucklescript-is-rebranding), so you will see some references to `bs` scattered around for now.

Let's add a couple scripts to run ReScript:
```json
// package.json
"scripts": {
  ...
  "res:dev": "rescript build -w",
  "res:build": "rescript"
}
```

Here's a PR for the initial setup: https://github.com/jderochervlk/rescript-counter-app/pull/1

## Where to start?
Let's first try and get the app running for local development.
```
npm run start
```
I have an issue with the older version of `react-scripts`. Updating to v5 fixes it and the app is running!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dx1z09cxh4ohu2do32ql.png)

I'll open up a new terminal tab and run `npm run res:dev` to kick off the ReScript compiler in watch mode.

With a TypeScript app we would be doing this conversion from a "breadth" first approach by changing all of the `.js` files to `.ts` files, leaving strict mode off, and slowly fix all of the `any` types and turn strict mode on. ReScript has a "depth" first approach where we will tackle one file at a time. We have to have all of the types in that file correct before we can compile and move onto the next file. It's usually easiest to do this by starting at the bottom of the code like small components or util functions.

The `NavBar` component looks like a good place to start.

## Navbar
Here's the starting code:
```js
import React from "react";

// Stateless Functional Component

const NavBar = ({ totalCounters }) => {
  return (
    <nav className="navbar navbar-light">
      <div className="navbar-brand">
        <i className="fa fa-shopping-cart fa-lg m-2" aria-hidden="true" />
        <span
          className="badge badge-pill badge-info m-2"
          style={{ width: 50, fontSize: "24px" }}
        >
          {totalCounters}
        </span>
        Items
      </div>
    </nav>
  );
};

export default NavBar;
```

It's a stateless functional component, so this should be really easy. I'm going to rename it to `.res` and work through the compiler errors.

- There are no imports in ReScript, every file has a [module](https://rescript-lang.org/docs/manual/latest/module) based on the filename. This means we can get rid of `import React from "react";`. We also delete the export.
- there isn't `const` in ReScript. Everything, including functions, uses [`let` binding](https://rescript-lang.org/docs/manual/latest/let-binding#sidebar). So we can change the `const` to `let`.
- we will change the name of the `NavBar` function to `make` and add the `@react.component` attribute to it. This tells the compiler that this is a [React component](https://rescript-lang.org/docs/react/latest/components-and-props), and we don't need to give it a name because it will come from the module name (the filename).
- The JS version uses destructured props `({ totalCounter })`. ReScript uses [labeled arguments](https://rescript-lang.org/docs/manual/latest/bind-to-js-function#labeled-arguments) so we need to change that to `(~totalCounters)`.
- ReScript doesn't use `return`. The [last expression in a block](https://rescript-lang.org/docs/manual/latest/overview#blocks-1) is returned. So we delete `return` and just leave the JSX expression.
- The `aria-hidden` tag isn't valid in ReScript and it needs to be changed to `ariaHidden` and the value needs to be boolean instead of a string: `<i className="fa fa-shopping-cart fa-lg m-2" ariaHidden=true />`
- In our style tag we can't use an int, it has to be a string. Everything in [ReScript can only be one type ](https://rescript-lang.org/docs/manual/latest/type)so style can't be a record of string or int, it has to just be a record of string.
- The text that says "Items" has to be wrapped in [`React.string`](https://rescript-lang.org/docs/react/latest/elements-and-jsx#element-basics). We can't just put text here because of the strict types, we have to pass it to a function that converts a string into a type of `React.element`. Don't worry, this doesn't exist in our compiled code.

Great! Now it compiles. Here's what it looks like:
```rescript
@react.component
let make = (~totalCounters) => {
  <nav className="navbar navbar-light">
    <div className="navbar-brand">
      <i className="fa fa-shopping-cart fa-lg m-2" ariaHidden=true />
      <span className="badge badge-pill badge-info m-2" style={{width: "50", fontSize: "24px"}}>
        {totalCounters}
      </span>
      {React.string("Items")}
    </div>
  </nav>
}
```

It's not that different from our original code and a JavaScript dev should easily be able to read it.

> Note: We're not putting type annotations on things because ReScript can [correctly infer all of our types](https://rescript-lang.org/docs/manual/latest/type#inference). Everything you see here has a type that is guaranteed to be correct. `totalCounters` is correctly inferred to be a `React.element` because that's how we use it later.

Now our React app is complaining that it can't find the `navbar.js` file. We just need to change the import in `App.js` to `import { make as NavBar } from "./components/navbar.bs";` and it will compile and render as expected.

Every thing looks good so far! Here's a PR with the changes: https://github.com/jderochervlk/rescript-counter-app/pull/2

> Note: When you are starting out with ReScript it is recommended to commit the compiled JavaScript along with the source ReScript code. This allows people to look at PRs and see the JS code and they should be able to contribute even if they don't fully understand everything in ReScript. It will help you and your team learn the language together.

## Counter
This one will be a bit more tricky because it's a class component. ReScript only has functional components, so we'll have to refactor it. I'll do that and then convert it to ReScript.

{% collapsible Here's our starting refactored component %}


```js
import React from "react";

function Counter({ counter, onIncrement, onDecrement, onDelete }) {
  let getBadgeClasses = () => {
    let classes = "badge m-2 badge-";
    classes += counter.value === 0 ? "warning" : "primary";
    return classes;
  };
  
  let formatCount = () => {
    const { value } = counter;
    return value === 0 ? "Zero" : value;
  };

  return (
    <div>
      <div className="row">
        <div className="">
          <span style={{ fontSize: 24 }} className={getBadgeClasses()}>
            {formatCount()}
          </span>
        </div>
        <div className="">
          <button
            className="btn btn-secondary"
            onClick={() => onIncrement(counter)}
          >
            <i className="fa fa-plus-circle" aria-hidden="true" />
          </button>
          <button
            className="btn btn-info m-2"
            onClick={() => onDecrement(counter)}
            disabled={counter.value === 0 ? "disabled" : ""}
          >
            <i className="fa fa-minus-circle" aria-hidden="true" />
          </button>
          <button
            className="btn btn-danger"
            onClick={() => onDelete(counter.id)}
          >
            <i className="fa fa-trash-o" aria-hidden="true" />
          </button>
        </div>
      </div>
    </div>
  );
}

export default Counter;
```
{% endcollapsible %}

### getBadgeClasses
ReScript doesn't have a `+=` operator (values are immutable) so we can just concatenate the string using `++`. Before we can access `counter.value` we need to declare a type for it. We don't have to annotate our function with that type because the compiler will connect them together for us.
```rescript
type counter = {value: int}

...

let getBadgeClasses = () => {
  "badge m-2 badge-" ++ {counter.value === 0 ? "warning" : "primary"}
}
``` 

### formatCount
We can't concatonate a string with an int like we can in JavaScript so we need to convert our value to a string first.

```rescript
let formatCount = () => {
  counter.value == 0 ? "Zero" : counter.value->Int.toString
}
```

> Note: `->` is the [pipe](https://rescript-lang.org/docs/manual/latest/pipe#sidebar) operator. It allows us to take a value and pass it as the first argument of the next function. 

### The rest
Clean up our JSX and fix the types passed to DOM elements and update the import in `counters.jsx` and it will render and work correctly.

PR is here: https://github.com/jderochervlk/rescript-counter-app/pull/3

## Counters
This one is also a class component that we need to refactor first, which just takes a minute since it doesn't have state.

We want to use the `counter` type from `counter.res` so we will add a type annotation to our prop:
```rescript
let make = (~onReset, ~onIncrement, ~onDelete, ~onDecrement, ~counters: array<Counter.counter>, ~onRestart) => { ... }
```

If a type comes from another module you need to usually add an annotation or [`Open` that module](https://rescript-lang.org/docs/manual/latest/module#opening-a-module) so inference will work.

Since we are using `counter` as the shared type from the `Counter` module we will rename it to `t`, which is the normal way in ReScript to say "this is the main type of this module". It's easier to read and use `Counter.t` for the type instead of doing `Counter.counter`.

Here's the PR for `Counters`: https://github.com/jderochervlk/rescript-counter-app/pull/4

## App.js
This is another class component we will need to refactor first. It has state and some methods to manage the state. To make this easier and avoid doing rework I will comment out the internals of those functions for now and rework each one into rescript.

Converting it over to a functional component and refactoring it to ReScript allowed us to clean up the state and state management functions to not mutate values and be more declarative.
```rescript
  let (counters, setCounters) = React.useState(() => [
    {id: 1, value: 0},
    {id: 2, value: 0},
    {id: 3, value: 0},
    {id: 4, value: 0},
  ])

  let handleIncrement = counter => {
    setCounters(prev => prev->Array.map(c => c.id != counter.id ? c : {...c, value: c.value + 1}))
  }

  let handleDecrement = counter => {
    setCounters(prev => prev->Array.map(c => c.id != counter.id ? c : {...c, value: c.value - 1}))
  }

  let handleReset = () => {
    setCounters(prev =>
      prev->Array.map(c => {
        ...c,
        value: 0,
      })
    )
  }

  let handleDelete = counterId => {
    setCounters(prev => prev->Array.filter(c => c.id !== counterId))
  }
```

### handleReset
This function uses `window.location.reload()` which is a DOM api. We don't currently have any DOM bindings installed and available for ReScript. Let's go ahead and install some [bindings](https://github.com/TheSpyder/rescript-webapi).

```rescript
open Webapi.Dom
open Location

...

let handleRestart = () => {
  location->reload
}
```

Here's the PR for `App.js`: https://github.com/jderochervlk/rescript-counter-app/pull/5

## index.js
This is what it looks like as `index.res`:
```rescript
%%raw("import './index.css'")
%%raw("import 'bootstrap/dist/css/bootstrap.css'")
%%raw("import 'font-awesome/css/font-awesome.css'")

switch ReactDOM.querySelector("#root") {
| Some(rootElement) => {
    let root = ReactDOM.Client.createRoot(rootElement)
    ReactDOM.Client.Root.render(root, <App />)
  }
| None => ()
}
```

Since we don't have a way to import `.css` files into ReScript we can use the `%%raw` expression to tell ReScript that we have a little snippet we aren't going to compiler. [Here are some docs](https://rescript-lang.org/docs/react/latest/styling#global-css) on this.

The way this CRA works it needs an `index.js` file, so `index.bs.js` won't work. Thankfully we can just update `bsconfig.js` and it will change all the extensions for us.
```json
{
  ...
  "suffix": ".js",
  ...
}
```

Here's our final PR: https://github.com/jderochervlk/rescript-counter-app/pull/6

## Wrapping up
We did it!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z331gf9wnhe7g3jub4or.png)

Now our app is fully typed and when saving a ReScript file the compiler updates in under 110 milliseconds so we didn't add any delays to our build process. If we run `npx rescript clean` and then `npx rescript` to do a fresh build from scratch it takes about 2 seconds.

We were able to break out the app and switch to ReScript slowly over multiple PRs and we didn't make any other changes to our application or tooling. This is still a CRA and we still use the standard `npm run start` to run it, and we can also eject it if we want to customize the configs later if we need to.

Feel free to ask any questions in the comments!