Issues with CRA Hot Reload + ReScript

tldr;

Checkout this sample repository: https://github.com/huntwj/rescript-cra-auto-reload-issue
First bad commit: 099b04b

Hello friends,

Initial context: converting the base CRA template from JS -> ReScript.

I’m trying to get the CRA hot reload to work with ReScript. Unfortunately it breaks when I extract an “external module” declaration from raw JavaScript.

The initial “Pseudo-port” of the .js files to .res files works fine, and the auto-reload continues to work fine. Once I extract an external module, however, it breaks.

This works:

%%raw(`import logo from './logo.svg';`)

This breaks auto-reload (though works with manual reload):

@module('./logo.svg') external logo: string = "default"

Note that the client still seems to detect “a change” and you see a network request on save, but the page itself does not change until you do a full reload on the page.

Any ideas and/or thoughts on how to proceed?

Thank you in advance,

Wil

P.S. For additional context, I’m following along with the following blog post:

What bs-platform version? And I assume you’re outputting to ES6? See [this page]. Generally speaking, have your js output open side by side and nudge your externals into whatever output you’d like to see.

I can’t help, but I can say I wrote the article and haven’t had any trouble with it. My first guess is that there is a race condition if cra reloads when it sees a file change before rescript has finished compiling. My machine is highly spec’d and rescript might be fast enough to beat cra for me. Another possibility is that rescript is not changing the output js file, so cra has nothing to reload. Checking what is happening in the compiled js output might help narrow it down.

Sorry for the delayed response. Per my yarn.lock file this is bs-platform@9.0.2.

I have since moved on, and while I no longer have simple replication instructions, I do regularly see a similar issue. What I do to “fix” the problem is close all .res files, manually delete the .bsb.lock file, and then re-open a .res file. I then get prompted to start a build and things work swimmingly, usually for the duration of my VS Code session. This is a pretty regular issue when I start VS Code however. Perhaps the .bsb.lock file is not being cleaned up when VS Code closes?

It would be nice if the VS Code extension had a command that simulated that and essentially forced a restart/rebuild of the internal state. Does one exist already perhaps?

Thanks all for your great work,

Wil

I’m also having an issue with ReScript generated files hot reloading in a CRA app.

Even broke it down to a very minimal example:

Here is Example.tsx and in it is the following:

function Example () {
	console.log("Example re-render")
  return <div>Hello World. Update? 3</div>
}

export default Example

An adjacent ReScript file for testing purposes:

@genType @react.compoent
let make = () => {
  Js.Console.log("Greeting Re-render")
  <div> {"Hello from ReScript."->React.string} </div>
}

@genType
let greeting = make

@genType let default = greeting

Finally my index.tsx:

// import App from "./App.bs";
import ReactDOM from "react-dom";
import React from "react";
import reportWebVitals from "./reportWebVitals";
import Example from './Example';
import Greeting from './Greeting.gen';

ReactDOM.render(<React.StrictMode><Example /> <Greeting /></React.StrictMode>, document.getElementById("root"))

reportWebVitals();

Reproduction

  1. Refresh the browser
  2. Edit Example.tsx and change the message, then save the file

It reloads as expected

  1. Edit the Greeting.res and change the message, then save the edits

It does not reload, a message is received by the websocket listener but nothing beyond that.

  1. Try to edit the message of Example.tsx and then save the file

It does not reload. Once I update my Greeting.res file and make changes to Example.tsx it stops reloading.

If I refresh the browser, I can make any number of changes to Example.tsx and it consistently reloads until I edit Greeting.res.

Same behavior on Node 14 and Node 16.

Any thoughts. Should I create an issue somewhere?

:flushed: Turned out to be that in this case, though I am still having issues with the actual project I’m working on so maybe I’ve got similar typos somewhere along the way?

If it’s messed up somewhere and it tries to hot reload, it will break all other reloads for previously working component updates.

Just lost half the day trying to track it down, fortunately I did find out the cause and a workaround.

The problem is caused by the resulting JS exports:

// make is your react component

var x = 5; // minimal example but in your reproduction repo it would be the logo external binding

export {
  make,
  x
}

For some reason when there is more than one export, CRA cannot hot reload changes to that component. The same happens if using export const

I found a workaround but it’s not ideal long-term. Create a .resi file to ensure only the make function is exported:

Create an App.resi

let make: {.} => React.element

I don’t like this as I already have many React components, and I’ll have to edit these files anytime a prop changes if I decide hot-reloading is worth it.

Just created an issue on the CRA repo [BUG]: Multiple exports break hot reloading · Issue #11087 · facebook/create-react-app · GitHub so we’ll see how this plays out.

Any better solutions come to mind out there?

I guess you are breaking the rules of “Fast Refresh” which state that a module should only export React components, otherwise the mechanism cannot reliably refresh.

Unfortunately this is not a bug and we need to hide all the exports that are not React components by using an interface file. Or you hide other stuff with the %%private extension, which is pretty adhoc.

Is there a way to use %%private to hide an external that is imported? That was the reason I encountered this issue:

@module(“./App.module.scss”) external styles: {..} = “default”;

@react.component
let make = () => {
  <div className={styles[“root”]}>
    I don’t really want to expose styles here
  </div>
}

Just doing that already creates two exports and breaks hot reloading.

Yep.

%%private(@module("./App.module.scss") external styles: {..} = "default")

@react.component
let make = () => {
  <div className={styles["root"]}>
    {React.string("I don’t really want to expose styles here")}
  </div>
}

JS Output:

var React = require("react");
var AppModuleScss = require("./App.module.scss").default;

var styles = AppModuleScss;

function Playground(Props) {
  return React.createElement("div", {
              className: styles.root
            }, "I don\xe2\x80\x99t really want to expose styles here");
}

var make = Playground;

exports.make = make;

Playground Link

1 Like

I think there’s some extra magic in the playground. I get a:

 Syntax error!
 [...]rescript-cra-auto-reload-issue/src/App.res:18:66

  16 │ // @module("./logo.svg") external logo: string = "default"
  17 │
  18 │ %%private(@module("./logo.svg") external logo: string = "default")
  19 │
  20 │ @react.component

  I'm not sure what to parse here when looking at ")".

FAILED: cannot make progress due to previous errors.

The repo I’m working on is using 9.0.2. The Playground has 9.1.2, but seems to still work when dropping it back to 9.0.2 so perhaps this is a special case by the Playground? Is there a workaround this to get it compiling through the CLI?

Edit: This was one of the first solutions I tried before posting:

What I’ve tried so far:

Attempt #1: Wrap external with %%private

%%private(@module("./App.module.css") external styles: {..} = "default")

But it causes a syntax error and has no idea what ) means in this context.

Attempt #2: Create inline module types for submodules

module View: {
  let make: {.} => React.element
} = {
  @module("./logo.svg") external logo: string = "default"

  @react.component
  let make = () => {
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          {React.string("Save and reload")}
        </p>
      </header>
    </div>
  }
}

let make = View.make

Attempt #3: Create a .resi file to only specify the make function

  1. Create an Example.res

    @react.component
    let make = () => <div>{"Hello World"->React.string}</div>
    
    
  2. Generate a .resi file

    bsc -i src/Example.res
    
    

Then I hit an error from bsc having no idea what React is. Perhaps this path can work with the proper CLI flags?

Nevermind. I got it working! The original reproduction repo was using the bsb command and the bs-platform, where now we should be using the rescript package. Once I switched to rescript@9.1.3 that syntax compiled just fine!

Yeah there might have been some syntax bugfixes in between those releases