Best practice developer copilot rules for React?

Can folks share what they are using for rules files in “developer copilot” enabled IDEs like Cursor, Windsurf, etc… Anybody found anything helpful beyond the non-rule based interactions?

I had these rules a while ago in Cursor:

- You use the latest ReScript v12 alpha.
- Ensure suggestions match this version. Refer to the indexed ReScript manual.
- Ensure any produced JSX matches the ReScript JSX syntax.
- Never ever use the `Belt` or `Js` modules, these are legacy.
- Always use the `JSON.t` type for json.
- Module with React components do require a signature file (`.resi`) for Vite HMR to work.
  Only the React components can be exposed from the javascript.
- In ReScript you cannot add text as child of a react component directly.
  `<h1 className="text-4xl font-bold mb-4">404 - Page Not Found</h1>` is wrong. The parser does not accept this.
  It should be `<h1 className="text-4xl font-bold mb-4">{React.string("404 - Page Not Found")}</h1>`.
  Everything should be a `React.element` in ReScript. So you need to use primitive conversion functions like `React.string`, `React.int`, `React.float` & `React.array`.
- When dealing with promises, prefer using `async/await` syntax.
- `React.useState` in ReScript takes a function when invoked. `let (age, setAge) = React.useState(_ => 4)`
- Every expression inside an interpolated string is expected to be of type string. You can't do `\`age = ${42}\``.
  You need to convert that number to a string. `\`age = ${(42)->Int.toString}\``
- In ReScript `type` is a keyword, so it can't be used a prop name in a JSX prop. Use `type_` if you need it. For example `<button type_="Submit"></button>`
7 Likes

Thank you - always good to have a head start!

1 Like

I can’t seem to edit my answer, so posting my recent list here again:

  • You use the latest ReScript v12 rc.
  • Ensure suggestions match this version. Refer to the indexed ReScript manual.
  • Ensure any produced JSX matches the ReScript JSX syntax.
  • Never ever use the Belt or Js modules, these are legacy.
  • Always use the JSON.t type for json.
  • Module with React components do require a signature file (.resi) for Vite HMR to work.
    Only the React components can be exposed from the javascript.
  • In ReScript you cannot add text as child of a react component directly.
    <h1 className="text-4xl font-bold mb-4">404 - Page Not Found</h1> is wrong. The parser does not accept this.
    It should be <h1 className="text-4xl font-bold mb-4">{React.string("404 - Page Not Found")}</h1>.
    Everything should be a React.element in ReScript. So you need to use primitive conversion functions like React.string, React.int, React.float & React.array.
  • When dealing with promises, prefer using async/await syntax.
  • React.useState in ReScript takes a function when invoked. let (age, setAge) = React.useState(_ => 4)
  • We are using the jsx preserve mode, meaning our output JavaScript file will have JSX.
    Although this looks quite machine generated.
  • Every expression inside an interpolated string is expected to be of type string. You can’t do \age = ${42}`. You need to convert that number to a string. `age = ${(42)->Int.toString}``
  • In ReScript type is a keyword, so it can’t be used a prop name in a JSX prop. Use type_ if you need it. For example <button type_="Submit"></button>
  • There are global helpers defined in src/Iluvatar.res, these should always be used when possible. No need to open Iluvatar, this is done in via rescript.json. (project specific)
  • External bindings with @send that return a value do not require () to be invoked.
type t

@send
external foo: t => string = "foo"
external i: t = "t"
let b = i->foo // Notice no `()` here!
  • When you see include in ReScript source code, it means a module function was used. This brings additional symbols into the current module scope that aren’t visible in the original source.
  • ReScript uses namespaces (defined in rescript.json) to solve module naming conflicts. Since ReScript only allows one module name per project, namespaces let you have modules with the same name in different folders. In code, namespaces look like MyNamespace.MyModule.myType, but internally they’re stored as MyModule-MyNamespace.myType during compilation.
  • ReScript does not support F#-style generic type parameters, either on the definition or the calling side.
    You can’t write let id<'a> = x => x or call functions like id<int>(5).
    Generics are always inferred automatically by the compiler.
    If you want to type a binding, put the type annotation after the pattern, not as a generic.
    For example:
// ✅ Correct
let id: int => int = x => x

// ❌ Wrong (F# style)
let id<'a> = x => x
let y = id<int>(5)
3 Likes