0

first off to give some context of my situation...I've got a library containing a few models etc for my application, then an electron application which adds some UI, the electron app also loads a custom JS file which allows for adding additional logic.

On my Workspace model I'm using the singleton pattern (I know, I know) to store an instance of the workspace. When the app starts up, it's loading the workspace, then calls document.head.appendChild to load my custom script.

In the custom script, I'm requiring that same model library to get my Workspace class. However, Workspace.instance is returning null despite it definitely being not null before.

By contrast, if before loading the script I do global.workspace = workspace, then the script can access the instance fine. So can anyone tell me what's going on? As far as I understood, static properties and methods have essentially global scope, though this seems to suggest otherwise.

In my library:

export default class Workspace {
    static instance: Workspace = null;

    startupData: any;

    constructor(startupData: any) {
        this.startupData = startupData;
        Workspace.instance = this;
    }
}

In my app:

import Workspace from 'mymodels';

const workspace = new Workspace(startupData);
global.workspace = workspace;
console.log(Workspace.instance);    //Returns my workspace instance

const script = document.createElement('script');
script.onload = showUI;
script.src = 'file:///' + workspace.startupData.logic;
document.head.appendChild(script);

In my custom script:

const mymodels = require('mymodels');

console.log(mymodels.Workspace.instance);    //Returns null
console.log(global.workspace);               //Returns my workspace instance
JCoyle
  • 100
  • 9

1 Answers1

2

When the app starts up, it's loading the workspace, then calls document.head.appendChild to load my custom script.

That would be fine if using native modules, but I suspect you're using a bundler of some kind (Webpack, etc.). I suspect you're ending up with two different copies of your module loaded: One as part of the initial app bundle, then another loaded natively by the browser's module handling. Since those are separate modules (as far as the browser knows), you end up with two copies of Workspace and thus two copies of its instance property.

Depending on the bundler, there's usually a way to tell it to handle this, but the details vary by bundler.


But, answering the question of scope: A public static property like your instance can be accessed anywhere its containing class (constructor function) can be accessed, because it's a property of that function object.

If by "scope" you meant "lifecycle," the static property is created when the class (constructor function) is created, and released once the class (constructor function) is released (or the property is removed from it via delete).


Just as a side note, your Workspace class isn't a singleton. Every time you do new Workspace, you'll create a new instance (and overwrite the previous value that was in Workspace.instance. To make it a singleton, you need to add a check to the constructor:

constructor(startupData: any) {
    if (Workspace.instance) {
        return Workspace.instance; // Return the singleton
    }
    this.startupData = startupData;
    Workspace.instance = this;
}

Or better yet, make your singleton more idiomatically (from a JavaScript perspective) by simply directly exporting an object:

export default const workspace = {
    // ...properties and methods...
};

or if you need class features like private fields:

export default const workspace = new class Workspace {
    // ...constructor (if desired), properties, and methods...
}();
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • "*If you need class features like private fields*", then I'd still [recommend not to use `new class`](https://stackoverflow.com/a/38741262/1048572) but rather a local (module-scoped) variable that's simply not exported. – Bergi Jun 26 '21 at 15:01
  • @Bergi - Some folks prefer class-like semantics, particularly for private methods, etc. YMMV :-) – T.J. Crowder Jun 26 '21 at 15:18
  • In that case, they should also enforce their singleton semantics in the constructor though – Bergi Jun 26 '21 at 15:25
  • @Bergi - I guess if they want to defend against `new workspace.constructor`. Edge case. – T.J. Crowder Jun 26 '21 at 15:32
  • @T.J.Crowder thanks so much for the detailed response. Yes I am using webpack to bundle the app. My bundling knowledge is quite limited, but wouldn't I only get that problem if the custom script was also getting bundled? Currently it's just a standalone .js, so I assumed that its call to require('mymodels') was just grabbing the module ref from the one already loaded into the main app context. If this is the problem though, do you know how I would handle this in webpack? – JCoyle Jun 26 '21 at 19:57
  • Also, re the point on the singleton, yeah it's a very lazy implementation with no safety checks at the moment just because it only gets loaded once, but it should be done better. I'll try out those more "javascript" ways of writing it, I'm very much still in the C# state of mind when I write js at the moment. – JCoyle Jun 26 '21 at 19:57
  • Upon further testing, I think you're right re the separate references to the same module. By also adding the Workspace class to global, in the custom script testing global.WorkspaceClass === require('mymodels').Workspace returned false. For now in the main app I'm just adding the entire module to global, so that in the script I can use global.mymodels in place of require('mymodels'). It doesn't feel like a particularly good solution and I'd be really interested if you do know a better approach, but it works and this is a small personal project so for now I'm saying "good enough". – JCoyle Jun 26 '21 at 20:29
  • @JCoyle - That's good debugging! *"...wouldn't I only get that problem if the custom script was also getting bundled..."* Actually it's the other way around: If it were being bundled, then Webpack would know to hook it up to the copy of the `Workspace` module Webpack bundled. FWIW, I agree with your assessment of the workaround (though workarounds are really helpful). Sadly I've never gotten very deep into Webpack config, but I know that if you include the custom script in the bundle but tell Webpack to put it in its *own* bundle, Webpack will know to hook things up correctly. – T.J. Crowder Jun 27 '21 at 07:42
  • @JCoyle - I think that's covered [here](https://webpack.js.org/guides/code-splitting/#root) and in linked parts of the docs. Happy coding! – T.J. Crowder Jun 27 '21 at 07:42
  • Thanks @T.J.Crowder I think that code splitting approach isn't quite what I'm going for since I don't want the custom script (of which there could be many) to have to be referenced in the main app and the whole thing re-bundled. At least I've got things working and slightly better understand how the bundling side of things works now. – JCoyle Jun 27 '21 at 14:00
  • @JCoyle - It doesn't have to be. You can set up the bundles as: A) Your app, and B) Your "custom script." Telling Webpack that, it will put the module(s) they share in a third bundle that either (but not both) will load. – T.J. Crowder Jun 27 '21 at 16:29