3

I want to share an api instance across multiple modules and be able to initialize it with external configuration. My code uses Webpack and Babel to transform those nice ES6 modules into something usable by browsers. I'm trying to achieve this:

// api.js
let api = null;
export default api;
export function initApi(config) {
  // use config to configure the shared api instance (e.g. with api base url)
  api = ...
}


// ======================
// entry.js
import { initApi } from './api';
import App from './App';

// Initialize the single shared instance before anyone has the chance to use it
const apiConfig = ...
initApi(apiConfig);

// Create the app and run it


// ======================
// App.js
// RootComponent has an import dependency chain that eventually imports DeeplyNestedComponent.js
import RootComponent from './RootComponent';

// Actual App code not important


// ======================
// DeeplyNestedComponent.js
// PROBLEM! This "assignment" to the api var happens before initApi is run!
import api from '../../../../api';

api.getUser(123); // Fails because "api" stays null forever even after the initApi() call

The "problem" occurs because ES6 modules are imported statically and import statements are hoisted. In other words, simply moving the import App from './App' line below initApi(apiConfig) doesn't make the import happen after initApi is called.

One way to solve this is to export an object from api.js (or in another globals.js file if I have multiple such shared objects with the same pattern) instead of a single variable like this:

// api.js
const api = {
  api: null,
};
export default api;
export function initApi(config) {
  // use config to configure the shared api instance (e.g. with api base url)
  api.api = ... // <-- Notice the "api." notation
}


// ======================
// DeeplyNestedComponent.js
// api is now the object with an empty "api" property that will be created when initApi() is called
import api from '../../../../api';

api.api.getUser(123); // <-- Ugh :(

Is there a way to achieve initialization of a shared service instance elegantly when using ES6 modules?

In my case, DeeplyNestedComponent.js must still import the api instance somehow. In other words, there is unfortunately no context object passed from App all the way down to DeeplyNestedComponent.js that could give access the api instance.

bernie
  • 9,820
  • 5
  • 62
  • 92

1 Answers1

3

The problem with your code is that

let api = null;
export default api;

does export the value null in the implicitly generated binding for the default export. However, you can also export arbitrary bindings under the name default by using the syntax

let api = null;
export { api as default };

This will work as expected. But you still need to make sure that no module accesses this export before you called initApi.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    Thanks for the answer. I tried the alternative syntax just in case and it gives the same result. Since the imports are static and hoisted, how can I delay accessing this export until _after_ the call to `initApi`? That's the core of my question. – bernie Jan 02 '21 at 17:02
  • Don't access it in top-level code of a module, but only inside functions, and then don't call these functions until after `initApi` is called. – Bergi Jan 02 '21 at 17:03
  • The only alternative is to load your modules in the correct order, i.e. first the module that calls `initApi()` (not *entry.js*) and then the modules that use `api` – Bergi Jan 02 '21 at 17:04
  • I must have done something wrong in my initial test of your suggestion. I just retried it and it seems to work now. For the record, I'm not accessing `api` anywhere before `initApi` is called. I'm gonna spend a little more time on this because I didn't expect what you suggested to actually make a difference! There's clearly something I don't understand. – bernie Jan 02 '21 at 20:36
  • According to [Exploring ES6](https://exploringjs.com/es6/ch_modules.html#_default-export-style-2-default-exporting-values-directly), `export default «expression»;` is equivalent to `const __default__ = «expression»; export { __default__ as default };` So I guess that is only true when applied to `const` vars? – bernie Jan 02 '21 at 20:40
  • Maybe have a look at https://stackoverflow.com/a/43987935/1048572 or https://stackoverflow.com/a/58441210/1048572 – Bergi Jan 02 '21 at 20:40
  • 1
    "*`export default «expression»;` is equivalent to `const __default__ = «expression»; export { __default__ as default };`*" - yes, exactly. And when your «expression» (`api`) is evaluated, it still has the value `null`, so `__default__` is initialised to `null` and re-assigning `api` later doesn't change the exported constant. – Bergi Jan 02 '21 at 20:42
  • You are absolutely right! Now that I read it again more carefully, it does exactly what you said. However, it's a pretty subtle (and unexpected for me!) difference of behavior between the two syntaxes that will certainly trip up people. Thanks again! – bernie Jan 02 '21 at 21:34