Introduction
I’m currently working on a frontend project using ReScript and React 19. While I consider myself a competent React developer, I appreciate having tools that help maintain best practices. A linter proves invaluable in ensuring I follow the rules of React, and since my instinct for React memoization isn’t perfectly tuned yet, I’m leveraging the new React compiler to handle that optimization.
ReScript doesn’t include built-in linting or analysis tools, so in this post, I’ll explain how to set up eslint to analyze the JavaScript output.
For this setup, I’m using Bun as my JavaScript runtime and package manager.
Installation
At the time of writing, I’m using ReScript 12 (alpha version). To follow along, create a new Vite project using:
bun create rescript-app@next
Select Vite and the latest v12 alpha version when prompted.
Next, install the required dependencies:
bun i -D eslint eslint-plugin-react-compiler eslint-plugin-react-hooks
⚠️ Ensure you’re using React 19:
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
ESLint Configuration
In our rescript.json
, we’ve set the suffix
to ".res.mjs"
. We’ll need to configure ESLint to process these files in our eslint.config.js
:
import reactHooks from "eslint-plugin-react-hooks";
import reactCompiler from "eslint-plugin-react-compiler";
export default [
{
files: ["src/*.res.mjs"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: {
"react-hooks": reactHooks,
"react-compiler": reactCompiler,
},
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react-compiler/react-compiler": "error",
},
},
];
To run the linter:
bunx rescript
bunx eslint
Let’s verify our setup with a test component in src/Hello.res
:
let someCheck = () => Math.random() == 1.
@react.component
let make = () => {
if someCheck() {
let (v, s) = React.useState(_ => 1)
}
<h1> {React.string("hey")} </h1>
}
Running bunx eslint
will show:
/our-project/src/Hey.res.mjs
12:5 error Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) react-compiler/react-compiler
12:5 error React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
✖ 2 problems (2 errors, 0 warnings)
React Compiler Configuration
To integrate the React compiler, modify your vite.config.js
:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
const ReactCompilerConfig = {};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
include: /\.res\.mjs$/,
babel: {
plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]],
},
}),
],
});
This configuration ensures the compiler transforms our JavaScript code when the browser requests modules. Note: The React compiler currently doesn’t recognize compiled JSX.
To work around this limitation, we need to annotate our components with the 'use memo'
directive:
@react.component
let make =
@directive("'use memo'")
(~count) => {
let (double, setDouble) = React.useState(_ => 2 * count)
<div> {React.string(`Hello`)} </div>
}
This compiles to:
import * as React from "react";
import * as JsxRuntime from "react/jsx-runtime";
function Playground(props) {
"use memo";
let count = props.count;
React.useState(() => count << 1);
return JsxRuntime.jsx("div", {
children: "Hello",
});
}
And transforms to:
import { c as _c } from "react/compiler-runtime";
import * as React from "react";
import * as JsxRuntime from "react/jsx-runtime";
function Playground(props) {
"use memo";
const $ = _c(3);
const count = props.count;
let t0;
if ($[0] !== count) {
t0 = () => count << 1;
$[0] = count;
$[1] = t0;
} else {
t0 = $[1];
}
React.useState(t0);
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = JsxRuntime.jsx("div", { children: "Hello" });
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
Conclusion
ESLint and the React Compiler are powerful tools for improving React code quality. While native ReScript tooling for these features would be ideal, this setup effectively catches potential issues and provides valuable optimizations. The combination of static analysis and automated performance improvements helps create more reliable React applications.