One day, WHATWG Loader Spec will be finished and hopefully will provide the all necessary APIs for easily implementing hot module replacement. But even today, native JavaScript modules have a few interesting properties, which make it possible in some cases. First, imports are live bindings:

In this example, every time we change counter , all other modules dependent on it receive the new value as if both counter and increment function were defined in the same module.

Second property, important for reloading part: module names are URLs and different URLs are considered different modules even when in the end they resolve to the same file. To demonstrate it, let’s slightly change the previous example:

Since counter is now imported from a different URL, increment call does not change it. counter.mjs and counter.mjs?someQueryParameter are two independent modules. If we look at the network tab in browser dev tools we will also see them as two different requests:

Knowing this, we can implement a server which will be able to hot-reload ES2015 module with a few restrictions (see below). It will work the following way:

The server will run a file watcher and the WebSocket endpoint. Every time module changes, it will broadcast changed module name and modification time to all connected clients.

On the front-end, we will implement the small WebSocket client, which will allow us to subscribe to updates to a particular module.

Under module.mjs?mtime=<anything> route we will serve the original module.

route we will serve the original module. Under module.mjs , instead of original source, we will generate and serve the proxy module with self-update capability:

We import every exported name from the original module. We assign our imports to the module-local variables. We use mutable let bindings so we can reassign them later. We export module-local variables under original names. We subscribe to the original module update notifications. Every time we receive update we re-import the module with a new mtime query parameter. Due to URL semantics, this will cause the browser to load the updated module. We update module-local variables to values we received after step 5.

This proxy module is the core idea of this technique. Since it has the same exports and served under the same URL, it can transparently replace the original. Due to a live bindings nature of ES2015 modules, all updates to the exports of the proxy module will be automatically received by every consumer.

This approach also has two limitations:

We can not update the module if export names or their number changed. In that case, we will fall back to full page reload If original module exports mutable bindings, like in our counter example, we can not generate the proxy module: since we reassign all the exports to different variables, they can not update properly like they do in original module. So, we don’t generate proxies for such modules and resort to full page reload if they change.

There are also few things I left out of this article: in the final demo, there is an API for reacting to module reload ( accept and dispose callbacks) and update notifications propagate to the module parent until they are handled.

You can find the source code and demo at my GitHub repo or try it on your own code with this npm package. Let me know what you think. Does this or any similar approach have any perspective?