Live reloading javascript modules without refreshing the browser

Live reloading javascript modules without refreshing the browser

Live reloading has been around for a decade or more and has made the lives of developers much easier and much more efficient.

However, while CSS updates are seamlessly refreshed into your page, I am yet to find a plug-in-play equivalent for javascript modules. This makes sense - CSS is stateless and furthermore sits completely outside of the DOM and Javascript “states”. Browsers are designed to inject CSS into a page at any point during the page lifecycle, so a “hot-reload” isn’t really an issue.


Note - some examples in this article are written in Typescript because, well, that’s how I write javascript these days. Remember however that Typescript is not executed by the browser - it is converted to regular ES5-compatible javascript before being sent down to the browser. The live-reloading is working against regular javascript, not Typescript.


Why live-reloading javascript is difficult

Javascript has many features which make hot-reloading much much more difficult. To name a few…

Javascript is stateful

If your module has any kind of property, then chances are that property is designed to change and hold its value through the course of the module’s lifetime.

Javascript is instantiated

Your browser will download javascript files, but these are not what you or your users are actually interacting with. More specifically, then are interacting with an instantiation of that file. Consider a module named PersonEditor.js:

export default class PersonEditor {
    constructor(){
      this.FirstName = "";
      this.LastName = "";
    }

    Save(){
      // Send to server....
      alert("Thanks, we have saved this record");
    }
}

It would be a simple matter to detect changes to this file and reload it to the browser, but it’s not the file that’s being used, instead it is an instantiation of it::

let editor = new PersonEditor();
editor.Save();

In this case, it is the editor object that you want to live-reload. This instantiation could be when the page loads, or it could be a dozen steps/pages into a Single Page Application. It is likely the result of a user interaction like a button click. There may be more than one current instantiation.

How on earth could the browser know any of this?

Fixing the instantiation problem

The answer to the above is that it is impossible for the browser to know how and where your modules are being used. It will always be vulnerable to the nuances of your particular coding style. So, to get around this, we flip it on it’s head - there is only one part of your code that knows where and when your modules are being used, and that is the modules themselves. Let’s write a base class that has a little more self-awareness than your average javascript module:

export default class BaseModule{
  constructor(){
    this.ListenForLiveReload();
  }
  
  ListenForLiveReload(){
    // Lo-fi listener just uses timeout
    setTimeout(() => {
      if (this.ReloadRequired) {
        let updatedModuleInstance = // See comments below
        Reload(updatedModuleInstance);
      }
      ListenForLiveReload();
    }, 500);
  }
  
  Reload(updatedModuleInstance){
    let mapper = new ClassMapper(this, updatedModuleInstance);
    mapper.Merge();
  }
}

export default class PersonEditor extends BaseModule{
  constructor(){
    super();
  }
  // ....etc, as before
}

Listening for the reload

Just a quick note here - in our implementation we have a FileWatcher in C# which pings out a notification over websockets to give us real-time reloads. But worst case, there’s nothing wrong with just downloading the file again every second or so, even if it hasn’t changed. Yes, it hammers your server but you’re only on your development machine and is far less than a production site would endure from 1000’s of concurrent of visitors.

Fixing the stateful problem

Okay, this is the juicy bit. I’ll give you the source code first:

export class ClassMapper<T> {
    private Original: T;
    private New: T;
    private SimplePropertyNames = ["string", "number", "boolean"];

    constructor(original: T, newItem: T) {
        this.Original = original;
        this.New = newItem;
    }

    private IsSimpleProperty(newProperty: any): boolean {
        if (newProperty == undefined) return true;
        var propertyTypeName = typeof(newProperty);
        if (this.SimplePropertyNames.indexOf(propertyTypeName) !== -1) return true;

        // Dates are type "object" so check for a function name
        if (typeof(newProperty["getDate"]) === "function" && typeof(newProperty["toISOString"]) === "function") return true;

        // Exception - jQuery - we *could* iterate through these complex objects, overwriting all methods etc, but that would be a waste of effort because we know they haven't changed
        if (typeof(newProperty["closest"]) === "function" && typeof(newProperty["parentsUntil"]) === "function") return true;

        // TODO - other exceptions as per jQuery above. Usually third-party plugins 
      
        // Default - must be a complex object
        return false;
    }


    private MergeProperty(propertyName: string): void {
        if (typeof (propertyName) !== "string") return;
        if (propertyName === "constructor") return;
        
        // Javascript base prototypes start prefixing functions with underscores, so this is a simple way to check if we've gone "beyond" our own code and into core javascript.  If you prefix your own function/property names with underscores then you'll need to find a different method
        if (propertyName.startsWith("_")) return ;

        var newProperty = this.New[propertyName];
        if (typeof (newProperty) === "undefined") {
            // console.log("Rejecting undefined property value for " + propertyName);
            return;
        }
        var propertyTypeName = typeof(newProperty);
        
        // Map the function
        if (newProperty instanceof Function) {
            // console.log("Updating function", propertyName);
            this.Original[propertyName] = newProperty;
            return;
        }

        // Simple property?
        if (this.IsSimpleProperty(newProperty)) {
            if (typeof (this.Original[propertyName]) === "undefined") {
                // console.log("Creating simple property", propertyName);
                this.Original[propertyName] = newProperty;
            } else {
                // Property exists - do not overwrite because we'd lose its value. Nothing to do!
                // console.log("Ignoring existing property", propertyName);
            }
            return;
        } 

        // If we have a complex property in the new module, but not in the old module, then we can just copy directly, including its value
        if (propertyTypeName === "object" && typeof (this.Original[propertyName]) === "undefined") {
            // console.log("Creating complex property", propertyName);
            this.Original[propertyName] = newProperty;
            return;
        }

        // Array
        if (newProperty.constructor === Array) {
            // console.group("Merging array");
            let originalArray = this.Original[propertyName] as Array<any>;
            let newArray = newProperty as Array<any>;

            for (var arrayIndex = 0; arrayIndex < newArray.length; arrayIndex++) {
                
                if (arrayIndex < originalArray.length) {
                    if (this.IsSimpleProperty(newArray[arrayIndex])) {
                        // Simple property - leave value
                        // console.log("Ignoring array item because it already exists", propertyName);
                        continue;
                    }

                    // Map complex object recursively through this class
                    let mapper = new ClassMapper(originalArray[arrayIndex], newArray[arrayIndex]);
                    mapper.Merge();
                } else {
                    // Add the item - the new module has more in the array than our old module
                    originalArray.push(newArray[arrayIndex]);
                }
            }
            // console.groupEnd();
            return;
        }

        // Merge complex object
        // console.log("Merging complex object", propertyName);
        let mapper = new ClassMapper(this.Original[propertyName], newProperty);
        mapper.Merge();
    }

    public Merge(): T {
        let prototype = Object.getPrototypeOf(this.New);
      
        // Complex properties include functions
        let complexPropertyNames = Object.getOwnPropertyNames(prototype);
      
        // Simple properties are things like strings and booleans
        let simplePropertyNames = Object.getOwnPropertyNames(this.New);
        let allProperties = complexPropertyNames.concat(simplePropertyNames);

        // console.group("Merging ");
        // console.log("Prototype", prototype);
        // console.log("Object", this.New);
        // console.table(allProperties);
        // console.log("-------------");
        
        // Iterate through each property/function we have found
        allProperties.map((propertyName: string) => {
            // console.group(propertyName);
            this.MergeProperty(propertyName);
            // console.groupEnd();
        });

        // console.groupEnd();

        return this.Original;
    }
}

I’ve commented the pertinent parts in code, but the gist is:

For properties

If the property is new, just add it and the value to the current module. If the property already exists then do nothing - we want to maintain the state of the current module.

For functions

Just override them - there is no “state” in a function, so this is pretty easy.

Summary

I can’t imagine a “one size fits all” solution for javascript merging, but you can see that with just a few lines of code in the appropriate place within your code base then it is possible to achieve live-reloading for javascript. We have been using this for a few months now and it works very well. The code above deals with the nuances of how we at Blackball Software build our modules and I expect you may have to tweak it for edge cases within your own code, but the principle remains sound.

Have fun.

How we saved over $23,000 in Microsoft Azure subscription costs

How we saved over $23,000 in Microsoft Azure subscription costs

Why developing the bare minimum is the right strategy

Why developing the bare minimum is the right strategy