question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Changes to support hot-reloading / React Refresh

See original GitHub issue

Background

A common complaint that I hear often against using Scala.js, is that the dev feedback cycle is really fast with JS but really slow with Scala. I’m referring to the amount of time it takes between modifying a source file and seeing the change reflected in a browser. The JS community have really endeavoured to get this time down to as fast as possible, both by the creation of dev servers that push updates to the browser immediately, and by hot-reloading tech that swaps out parts of a live app without requiring a page reload or loss of state (where possible). The result is that JS devs can enjoy effectively-instant updates in the browser when making local changes. Giving up this fast feedback cycle often seems to be a hard sell for people considering Scala.js.

Goal

I’m currently working on (hopefully) adding support for React Refresh (i.e. hot reloading) to scalajs-react. By combining Scala.js’s awesome ability to spit out fine-grained ES modules, and a fast bundler that supports hot-reloading (like Vite), I hope to provide a fast dev feedback cycle using Scala, that can stand up to the JS experience. It’s likely to be in the order of 500ms on the Scala side compared to 5ms in JS, which from a human’s pov should be equivalent enough. For large projects it might be 2 sec, but that’s way more acceptable than the current Scala status quo of around 20 sec.

Problems

After much experimentation and debugging, I was able to get Vite hot-reloading Scala.js, and it’s fast! The approach seems like to should work seamlessly with bundlers like Webpack, etc too.🥳

However, hacks were required; and by hacks, I mean part of what I had to do was manually hack and chop up the Scala.js output. These were the problems I encountered. For objects that contain scalajs-react components…

  • the object’s class needs to be in its own module, and be the sole and default export. – If Vite sees a non-default export, it won’t hot-reload that module.

  • the object’s class module needs to eagerly initialise the object. – Bundles will inject hot-reloading code into the module and expect it to be used by the time the module has finished loading. Calling the hot-reload code when Scala decides to initialise the module doesn’t work because the module has already loaded, and the hot-reloading code has removed itself by then.

  • when an object’s source is modified, only the object class’s module should be updated (which contains the object body), and not the module with object’s singleton-getter (which seems to be static boilerplate). – When Vite sees an update to the object’s class module, it will hot-reload it. When Vite sees an update to the object loader, it doesn’t find anything hot-reloadable and instead reloads the entire page, which loses all user state.

Proposed Solution

In order for scalajs-react (and presumably some other libraries too) to support hot-reloading, and hot-reloading without a page reload especially, some Scala.js changes seem unavoidable. As an initial draft proposal, I suggest the following:

  • A new ability to Scala.js that
    • for some Scala objects (see the next section for a concrete example)
      • separate the object class into its own module as the sole, default export
      • eagerly initialise the object when the class module is loaded
      • allow for replacement of object instances
    • decides which Scala objects to transform by, at a minimum, inspecting the object body, probably looking for a term (eg. scalajs.js.special.eagerlyLoad;). (An annotation won’t work because it needs to be generated in scalajs-react call-site. It needs to be something a macro can provide that users can store in a val.)
    • can be globally enabled/disabled via an sbt setting
    • will only work when modules are being emitted (maybe some other constraints?)
  • If the module emitting code doesn’t already have this, teach it to remember previous output (at least checksums) and avoid replacing files with the exact same content

Sample Changes In Output

Consider this example Scala source code:

package demo

object MyComponent {
  org.scalajs.dom.console.log("MyComponent initialising...")
}

Currently it emits a single module:

demo.MyComponent$.js:

'use strict';
import * as $j_java$002elang$002eObject from "./java.lang.Object.js";

/** @constructor */
function $c_Ldemo_MyComponent$() {
  $n_Ldemo_MyComponent$ = this;
  console.log("MyComponent initialising...")
}
export { $c_Ldemo_MyComponent$ as $c_Ldemo_MyComponent$ };
$c_Ldemo_MyComponent$.prototype = new $j_java$002elang$002eObject.$h_O();
$c_Ldemo_MyComponent$.prototype.constructor = $c_Ldemo_MyComponent$;

/** @constructor */
function $h_Ldemo_MyComponent$() {
  /*<skip>*/
}
export { $h_Ldemo_MyComponent$ as $h_Ldemo_MyComponent$ };
$h_Ldemo_MyComponent$.prototype = $c_Ldemo_MyComponent$.prototype;
var $d_Ldemo_MyComponent$ = new $j_java$002elang$002eObject.$TypeData().initClass({
  Ldemo_MyComponent$: 0
}, false, "demo.MyComponent$", {
  Ldemo_MyComponent$: 1,
  O: 1
});
export { $d_Ldemo_MyComponent$ as $d_Ldemo_MyComponent$ };
$c_Ldemo_MyComponent$.prototype.$classData = $d_Ldemo_MyComponent$;

var $n_Ldemo_MyComponent$;
function $m_Ldemo_MyComponent$() {
  if ((!$n_Ldemo_MyComponent$)) {
    $n_Ldemo_MyComponent$ = new $c_Ldemo_MyComponent$()
  };
  return $n_Ldemo_MyComponent$
}

export { $m_Ldemo_MyComponent$ as $m_Ldemo_MyComponent$ };

Hot-reloading compatible output would look like this:

  • demo.MyComponent$.js:

    'use strict';
    import * as $j_java$002elang$002eObject from "./java.lang.Object.js";
    
    // ****** NOTE: This snippet is moved into another file
    import $c_Ldemo_MyComponent$ from "./demo.MyComponent.js"
    export { $c_Ldemo_MyComponent$ as $c_Ldemo_MyComponent$ };
    
    /** @constructor */
    function $h_Ldemo_MyComponent$() {
      /*<skip>*/
    }
    export { $h_Ldemo_MyComponent$ as $h_Ldemo_MyComponent$ };
    $h_Ldemo_MyComponent$.prototype = $c_Ldemo_MyComponent$.prototype;
    var $d_Ldemo_MyComponent$ = new $j_java$002elang$002eObject.$TypeData().initClass({
      Ldemo_MyComponent$: 0
    }, false, "demo.MyComponent$", {
      Ldemo_MyComponent$: 1,
      O: 1
    });
    export { $d_Ldemo_MyComponent$ as $d_Ldemo_MyComponent$ };
    $c_Ldemo_MyComponent$.prototype.$classData = $d_Ldemo_MyComponent$;
    
    // ****** NOTE: A variable and singleton-logic is removed in favour of a global var lookup.
    // ******       These objects are always eagerly initialised and will always exist.
    function $m_Ldemo_MyComponent$() {
      return globalThis.scalaJsEagerObjects["demo.MyComponent"];
    }
    
    export { $m_Ldemo_MyComponent$ as $m_Ldemo_MyComponent$ };
    
  • demo.MyComponent.js:

    'use strict';
    import * as $j_java$002elang$002eObject from "./java.lang.Object.js";
    
    /** @constructor */
    function $c_Ldemo_MyComponent$() {
      // ****** NOTE: Here we add/replace ourselves in a global var instead of a local one
      // ******       hidden in another module.
      (globalThis.scalaJsEagerObjects ||= {})["demo.MyComponent"] = this;
      console.log("MyComponent initialising...")
    }
    $c_Ldemo_MyComponent$.prototype = new $j_java$002elang$002eObject.$h_O();
    $c_Ldemo_MyComponent$.prototype.constructor = $c_Ldemo_MyComponent$;
    
    // ****** NOTE: Here we eagerly initialise object.
    // *****        Also replaces an older version on hot-reload.
    new $c_Ldemo_MyComponent$()
    
    // ****** NOTE: Here we have a single default export.
    export default $c_Ldemo_MyComponent$;
    

Who Does The Work?

I might be able to put my hand up to implement this. It seems about a medium-difficulty change to me, but we’d have to work out how long we think it would take before I can say much more.

I think it’d be a really good idea to work together with Team Scala.js to come up with a plan that we all agree is effective and feasible. With your blessings and guidance, I hope we can at least get a spec-lite ready.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:11
  • Comments:12 (11 by maintainers)

github_iconTop GitHub Comments

1reaction
japgollycommented, May 31, 2022

FYI, I’ve had a million billion things on, tons of work-work and life-work stuff, everyone in the family getting sick every two seconds, but things are settling back down and so this issue is back on my radar again. I’ll start by looking at StandardLinkerImpl, thanks for the guidance @gzm0

1reaction
gzm0commented, Apr 3, 2022

Regarding the plugin idea: What you are describing in “Plugin Powers” can already be reasonably done using the linker.standard package.

A good starting point is probably StandardLinkerImpl:

https://github.com/scala-js/scala-js/blob/main/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerImpl.scala

Apart from all the bookkeeping, what it does is:

  1. Create a ModuleSet, using StandardLinkerFrontend.
  2. Pass that ModuleSet to StandardLinkerBackend

You can write your own implementation of Linker and do whatever you want between steps 1 / 2.

Read more comments on GitHub >

github_iconTop Results From Across the Web

React Fast Refresh — The New React Hot Reloader
When editing a React component, React Fast Refresh will efficiently only update and re-render that component. This leads to significantly faster ...
Read more >
Enabling hot reload in React web app | by Dong Chen - Medium
Hot reload allows developers to see result of code change in browser without page refresh. This greatly improves developer productivity and experience, ...
Read more >
Difference between Hot Reloading and Live ... - GeeksforGeeks
Hot reloading allows you to see the changes that you have made in the code without reloading your entire app. Whenever you make...
Read more >
Hot Reload is not working in my React App - Stack Overflow
I have created this app with npx create-react-app. After this i have deleted all the files except ...
Read more >
Set up React Hot Loader in 10 minutes - LogRocket Blog
A hot reload to an app will only refresh the files that were changed without losing the application's state. · A live reload...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found