[Help] Fast Refresh with Vite and ReScript React ecosystem

Hello!

I was wondering how do you guys handle HMR with Vite in the React ReScript ecosystem.
Both vite react plugins (vite-plugin-react and vite-plugin-react-swc) are using react-refresh under the hood which makes the hot reloading very inconsistent and often makes the dev server going crazy (it goes through every export of my bs files of my codebase trying some resolution for hmr update to work) where I have to resign to do a manual page reload.

Here’s why hmr invalidates (also explained in the Gatsby doc), putting everything manually in private prevents that issue as expected, but this is not a reasonable solution until RFC: private by default for values.

I feel like I’m most likely not the only one in our community encountering this problem using Vite. :pray:

1 Like

You are definitely not the only one.

Use interface files. You can generate them automatically with the create-interface command in rescript-vscode.

Then delete everything from it, except the signature for the React component’s make function, and fast refresh should work.

3 Likes

Guess I will have to do that, but handling interface files is quite tedious especially on large codebases, wrapping everything with %private(...) is also not a viable alternative.
The old GitHub - gaearon/react-hot-loader: Tweak React components in real time. (Deprecated: use Fast Refresh instead.) was more consistent although less reliable, unfortunately it is not available with Vite. :confused:

Interfaces are not as bad in reality as they seem conceptually, and the benefits are extensive and have a long tail (not just fast refresh, also compilation speed, maintainability, and more). I’m sure you know what will work for you, but if you haven’t given them a proper try I recommend it.

I think developers have been trained to dislike duplication, correctly; but this is not duplication in the same sense because it’s fully compiler-checked. You can’t get it wrong, and you don’t have to write it by hand.

4 Likes

I’m interested in whether there’s something we can do to make this work without interface files/managing visibility. Why is there a requirement for only one component per file and is there something we can do about that?

Here’s one example of the requirements: https://nextjs.org/docs/basic-features/fast-refresh

It’s slightly complicated by the fact that fast refresh is itself a low level thing that frameworks such as Vite or NextJS implement themselves (using facebook’s react-refresh package).

So using interfaces files to only export React components is okay, it seems to work constantly even with memo/forwardRef wrapping. Exporting values also won’t trigger a full page reload.

But what about functions? Two commons cases: functions that need to be exported for testing & constructor for record types. It seems the general approach is to never export and have them in separate files. However, this approach goes against the ReScript philosophy of using submodules and limiting files to keep the project flat.

Clearly, using a function to trick around the isLikelyComponentType and make the function displayName in PascalCase is a tradeoff I’m willing to accept to avoid separating these functions from their component file.

let hmr = fn => {
  let fnDict = fn->Obj.magic->Js.Dict.fromArray
  let name = fnDict->Js.Dict.unsafeGet("name")
  let updatedName = name->Js.String2.toUpperCase

  fnDict->Js.Dict.set("displayName", updatedName)
  fnDict->Obj.magic
}
let makeItem = (~label, ~key="", ~value=None, ~disabled=false, ()) =>
   { label, key, value, disabled }

@react.component
let make = () => ...

let make = React.memo(make)
let makeItem = Helpers.hmr(makeItem)

I wonder if it’d be feasible to maintain a fork of react-refresh that’s tailored to ReScript. Would feel better than playing constant catch up with these things, although we would of course be playing another type of catch up.

Maybe the JSX transform could add the displayName property directly. This has probably been considered before, but would be interesting to investigate.

I know it’s not the main topic of your answer, but I don’t like changing visibility of functions only for testing reasons, that’s why I created a small PPX that allows to write tests inline, that’s one more positive point for creating tight interface files.

2 Likes

We have been having trouble with spurious reloads. We use a patch for vite-rescript.

Here are my observations:
Vite (rollup?) seems to be really smart with finding module dependencies, if a javascript module A uses module B’s function B.doThing then a dependency edge AB is made. If B is changed (and invalidated in vite terms), then A will also be invalidated. Anything that depends on A will not be necessarily be invalidated this the change propagation will end.

Say you change the rescript file for B, and then let rescript complie your project by compiling B and then compiling A, because A depends on B’s doThing, then both B and A seems to be written to disk regardless of if content changes (I watched inotify). Typically this means that any file change will cause the root file to be recompiled through a path from the changed file to the root and finally trigger a HMR reload.

We get around this by skipping changes via a content-hash.

Any feedback is very welcome.