How to set up a React app using ReScript, Vite, and Tailwind for lightning fast development.
Tags: rescript, react, vite, tailwindcss
Published: 2023-09-24 17:23:15

[](url)Let's set up a modern React application using tools that give the fastest local development experience while still enabling strict type checking.

ReScript modules can almost instantly compile to [JavaScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) (ESM) which allows us to use a tool like [Vite](https://vitejs.dev/) to quickly load them directly into the browser when changes are made.

> Note: This guide will walk you through all of the steps manually to set up ReScript, Vite, and Tailwindcss. I suggest you do this at least once to learn how it all works, but you can also use [`npm create rescript-app`](https://github.com/rescript-lang/create-rescript-app#readme) to quickly create an app with these tools. Just select "vite" for a template and include the new `@rescript/core` library when asked.

## What is ReScript?
[ReScript](https://rescript-lang.org/) is a language that compiles to JavaScript with strong types, a speedy compiler, no null exceptions, and pattern-matching.

You can add it to your existing JS toolchain and build system, and it uses your standard JS package manager like NPM or Yarn.

Using it feels very similar to using TypeScript, but there are a few key differences to each language:

- ReScript is strongly typed, which means nothing can be of an `any` type or `unknown`. If something has a type it is guaranteed to be correct.
- TypeScript is a superset of JavaScript, which means that all valid JS is valid TS. While ReScript has a JS like syntax it is a different language, so most of the JS foot guns don't exist like knowing when to use `==` or `===`.
- While you can adopt both languages gradually into a project the approach is different. With TypeScript you start off without strict mode, rename the files from `.js` to `.ts`, slowly convert all of the `any` types to known types, and (hopefully) turn on strict mode. With ReScript you would convert a single file at a time.

You can see more differences [here](https://rescript-lang.org/docs/manual/latest/introduction#difference-vs-typescript).

## Set up your IDE
Installing a ReScript plugin into your IDE will give you syntax highlighting, auto-complete, and clicking to sources for functions, types, and values.

For VSCode you can search for and install the [ReScript extension](https://marketplace.visualstudio.com/items?itemName=chenglou92.rescript-vscode). If you are using a different IDE you can find instruction on the [ReScript site](https://rescript-lang.org/docs/manual/latest/editor-plugins).

## Set up the workspace
I'll be using npm, but you can swap this out for your package manager of choice.
```ssh
mkdir my-rescript-vite-app
cd my-rescript-vite-app
npm init -y
```

Open this folder in your IDE.

## Install ReScript
Let's install ReScript and the [ReScript Core library](https://github.com/rescript-association/rescript-core).
```ssh
npm i rescript @rescript/core
```
Now we have to create the ReScript config file.

> Note: ReScript is based on a combination of the [Reason and BuckletScript](https://rescript-lang.org/blog/bucklescript-is-rebranding), so you'll see `bs` used in places.

Create a `rescript.json` file in the root of the application.

```json
{
    "name": "my-rescript-vite-app",
    "sources": [
        {
            "dir": "src",
            "subdirs": true
        }
    ],
    "package-specs": {
        "module": "es6",
        "in-source": true,
        "suffix": ".mjs"
    },
    "bs-dependencies": [
        "@rescript/core"
    ],
    "bs-dev-dependencies": [],
    "bsc-flags": [
        "-open RescriptCore"
    ]
}
```
### Quick breakdown of the config options
- name: the name of our application
- sources: where our code lives
- package-specs: what module system we want to use and if we want the output to live in the same folder as the source
- bs-dependencies: similar to npm dependencies
- bs-dev-dependencies: similar to npm dev-dependencies
- bsc-flags: Additional options for the compiler
}

We've set up ESM files since we'll be using Vite, added `@rescript/core` as a dependencies, and we're opening up that module to make it easier to access later.

You can read the [full documentation on the ReScript site](https://rescript-lang.org/docs/manual/latest/build-configuration).

## Verify that ReScript compiles
We should be able to compile and run some code now!
Add a couple scripts to the `package.json` file:
```json
"scripts": {
  "res:build": "rescript",
  "res:dev": "rescript -w"
}
```
Create a file named `src/hello.res` and log something to the console:
```rescript
Console.log("Hello world!")
```
Compile the source code with `npm run res:build` and execute it with `node src/hello.mjs`.

You should see `Hello world!` print to the console.

## Install React
Install React, React Dom, and Rescript bindings for React.

```ssh
npm i react react-dom @rescript/react 
```
Then add `@rescript/react` as a dependency in `bsconfig.json` and configuration for JSX.
```json
"jsx": { "version": 4, "mode": "automatic" },
"bs-dependencies": ["@rescript/core", "@rescript/react"]
```

Let's change `hello.res` to be a React component and rename it to `App.res`.
```rescript
@react.component
let make = () => <p> {React.string("Hello world!")} </p>
```

We'll also want to make an [interface file](https://rescript-lang.org/docs/manual/latest/module) (`.resi`) for each React component. This enables Vite's hot module reloading (HMR) to [work as expected](https://forum.rescript-lang.org/t/help-fast-refresh-with-vite-and-rescript-react-ecosystem/4388/2) when making changes to our ReScript components.

> Note: For HMR to work correctly each file can only contain one component and cannot export any other functions.

## Install Vite
```ssh
npm i vite @vitejs/plugin-react -D
```

Create an `index.html` file in the project root:
```html
<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ReScript Vite App</title>
</head>

<body>
    <div id="root"></div>
    <script type="module" src="./src/Main.mjs"></script>
</body>

</html>
```

Create our main app at `src/Main.res`:
```rescript
switch ReactDOM.querySelector("#root") {
| Some(rootElement) => {
    let root = ReactDOM.Client.createRoot(rootElement)
    ReactDOM.Client.Root.render(root, <App />)
  }
| None => Exn.raiseError("No element has ID of #root!")
}
```

Create `vite.config.mjs` in the root of the project:
```js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(
    {
       include: ["**/*.mjs"], // since we're not using .jsx files we have to tell Vite to watch for HMR changes to .mjs files
    }
  )],
});
```
And finally let's add some more npm scripts to build our app:
```json
"scripts": {
    "res:build": "rescript",
    "res:clean": "rescript clean",
    "res:dev": "rescript build -w",
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
```

Open up two terminal windows or tabs. In the first one run `npm run res:dev` and in the second run `npm run dev`. If you are using VSCode with the ReScript extension installed it will ask you if you want to run a ReScript build server which you can use instead of running `npm run res:dev`. I choose to run it from the terminal so I can see both outputs next to each other easily.

You should now be able to open up `http://127.0.0.1:5173/` and see "Hello world!". Make a change to `App.res` and you should see Vite apply the changes with HMR.

## Adding Tailwind CSS
Starting by installing Tailwind and running the setup command.
```ssh
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

Add `html` and `mjs` files to the `tailwind.config.js` file:
```js
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{html,mjs}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

Add `src/main.css` with the basic Tailwind setup:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Import that file into `index.html`.
```html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="./src/main.css" rel="stylesheet">
    <title>ReScript + Vite + React</title>
</head>

<body>
    <div id="root"></div>
    <script type="module" src="/src/Main.mjs"></script>
</body>

</html>
```

Now we can use Tailwind classes in our ReScript components! Go ahead and add some to `App.res`.
```rescript
@react.component
let make = () =>
  <div className="text-3xl text-center m-5 text-gray-700"> {React.string("Hello world.")} </div>
```

## Wrapping up
You now have the basic shell to build out a React application quickly and with confidence. 

During local development we can see change appear in milliseconds instead of minutes or seconds, and we can do this while still having confidence in our code because of [ReScript's strong type system and compiler](https://rescript-lang.org/docs/manual/latest/introduction). ReScript removes the need for TypeScript (has types already), ESLint (ReScript doesn't have the "bad parts" of JS and the compiler helps us write clean code), and Prettier (ReScript has a built in formatter). Since we don't need these tools it speeds up VSCode because these don't have to run every time we save a file.

Make sure to check out other articles on [ReScript](https://dev.to/search?q=rescript&filters=class_name:Article)!