ericsoco
ericsoco

Reputation: 26253

How can I conditionally import an ES6 module?

I need to do something like:

if (condition) {
    import something from 'something';
}
// ...
if (something) {
    something.doStuff();
}

The above code does not compile; it throws SyntaxError: ... 'import' and 'export' may only appear at the top level.

I tried using System.import as shown here, but I don't know where System comes from. Is it an ES6 proposal that didn't end up being accepted? The link to "programmatic API" from that article dumps me to a deprecated docs page.

Upvotes: 368

Views: 273539

Answers (17)

Varun
Varun

Reputation: 602

One can go through the below link to learn more about dynamic imports

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#description

Upvotes: 0

thecodejack
thecodejack

Reputation: 13379

We do have dynamic imports as part of ECMAScript 2020. This is also available as babel-preset.

Following is way to do conditional rendering as per your case.

if (condition) {
    import('something')
    .then((something) => {
       console.log(something.something);
    });
}

This basically returns a promise. The resolution of promise is expected to have the module. The proposal also has other features like multiple dynamic imports, default imports, js file import etc. You can find more information about dynamic imports here.

Upvotes: 295

Kirill
Kirill

Reputation: 182

I had a similar situation. My project structure was like this:

  • libs /
    • mockApi.js
    • realApi.js
  • index.js

It was necessary for me that in production mode mocks did not get into the bundle. It was also important for me not to write conditions in every place of use and not to work with Promises.

For me the solution was to create a unifying api.js file with the code:

// solution 1
export const api = (
    await import(`${process.env.NODE_ENV === 'development' ? './mockAPI.js' : './realAPI.js'}`)
).default

export default api;

With this approach in production mode, only the processed realAPI.js gets into the bundle, and the use of the solution does not require separate conditions or work with Promises, for example:

import api from './libs/api';
api.getUser();

It is also possible to use a similar solution:

// solution 2
let api = (await import('./realAPI')).default;

if (process.env.NODE_ENV === 'development') {
    api = (await import('./mockAPI')).default;
}

export default api;

Both solutions allow not to include "mocks" in the bundle in production mode. This is done by removing unreachable code during the build process, important not to move the process.env.NODE_ENV === 'development' condition into a variable.

Upvotes: 1

Egg
Egg

Reputation: 101

As ericsoco say, instead of:

import {ExampleClass} from "./ExampleClass.js";
new ExampleClass();

you can write

if(condition) {
  import("./ExampleClass.js").then((module) => {
    new module.ExampleClass();
  });
}

Upvotes: 0

Leonardo Raele
Leonardo Raele

Reputation: 2814

2020 Update

You can now call the import keyword as a function (i.e. import()) to load a module at runtime. It returns a Promise that resolves to an object with the module exports.

Example:

const mymodule = await import('modulename');
const foo = mymodule.default; // Default export
const bar = mymodule.bar; // Named export

or:

import('modulename')
    .then(mymodule => {
        const foo = mymodule.default; // Default export
        const bar = mymodule.bar; // Named export
    });

See Dynamic Imports on MDN

Upvotes: 89

Gustavo Vargas
Gustavo Vargas

Reputation: 2597

I know this is not what the question is asking for, but here is my approach to use mocks when using vite. I'm sure we can do the same with webpack and others.

Suppose we have two libraries with same interface: link.js and link-mock.js, then:

In my vite.config.js

export default defineConfig(({ command, mode }) => {
    
    const cfg = {/* ... */}

    if (process.env.VITE_MOCK == 1) {
        cfg.resolve.alias["./link"] = "./link-mock"; // magic is here!
    }

    return cfg;
}

code:

import { link } from "./link";

in console we call:

# to use the real link.js
npm run vite

# to use the mock link-mock.js
VITE_MOCK=1 npm run vite

or

package.json script

{
    ....
    "scripts": {
        "dev": "vite",        
        "dev-mock": "VITE_MOCK=1 vite"
    }
}

Upvotes: 0

Santosh Subedi
Santosh Subedi

Reputation: 99

Import and Export Conditionally in JS

const value = (
    await import(`${condtion ? `./file1.js` : `./file2.js`}`)
).default

export default value

Upvotes: 7

Solo
Solo

Reputation: 6957

Important difference if you use dynamic import Webpack mode eager:

if (normalCondition) {
  // this will be included to bundle, whether you use it or not
  import(...);
}

if (process.env.SOMETHING === 'true') {
  // this will not be included to bundle, if SOMETHING is not 'true'
  import(...);
}

Upvotes: 8

Aldee
Aldee

Reputation: 4518

No, you can't!

However, having bumped into that issue should make you rethink on how you organize your code.

Before ES6 modules, we had CommonJS modules which used the require() syntax. These modules were "dynamic", meaning that we could import new modules based on conditions in our code. - source: https://bitsofco.de/what-is-tree-shaking/

I guess one of the reasons they dropped that support on ES6 onward is the fact that compiling it would be very difficult or impossible.

Upvotes: 0

Ashok R
Ashok R

Reputation: 20766

Look at this example for clear understanding of how dynamic import works.

Dynamic Module Imports Example

To have Basic Understanding of importing and exporting Modules.

JavaScript modules Github

Javascript Modules MDN

Upvotes: 0

Elliot Ledger
Elliot Ledger

Reputation: 468

Conditional imports could also be achieved with a ternary and require()s:

const logger = DEBUG ? require('dev-logger') : require('logger');

This example was taken from the ES Lint global-require docs: https://eslint.org/docs/rules/global-require

Upvotes: 3

bradley2w1dl
bradley2w1dl

Reputation: 91

I was able to achieve this using an immediately-invoked function and require statement.

const something = (() => (
  condition ? require('something') : null
))();

if(something) {
  something.doStuff();
}

Upvotes: 0

Shoaib Nawaz
Shoaib Nawaz

Reputation: 2320

require() is a way to import some module on the run time and it equally qualifies for static analysis like import if used with string literal paths. This is required by bundler to pick dependencies for the bundle.

const defaultOne = require('path/to/component').default;
const NamedOne = require('path/to/component').theName;

For dynamic module resolution with complete static analysis support, first index modules in an indexer(index.js) and import indexer in host module.

// index.js
export { default as ModuleOne } from 'path/to/module/one';
export { default as ModuleTwo } from 'path/to/module/two';
export { SomeNamedModule } from 'path/to/named/module';

// host.js
import * as indexer from 'index';
const moduleName = 'ModuleOne';
const Module = require(indexer[moduleName]);

Upvotes: 4

Chris Marstall
Chris Marstall

Reputation: 105

obscuring it in an eval worked for me, hiding it from the static analyzer ...

if (typeof __CLI__ !== 'undefined') {
  eval("require('fs');")
}

Upvotes: 1

BaptWaels
BaptWaels

Reputation: 1704

If you'd like, you could use require. This is a way to have a conditional require statement.

let something = null;
let other = null;

if (condition) {
    something = require('something');
    other = require('something').other;
}
if (something && other) {
    something.doStuff();
    other.doOtherStuff();
}

Upvotes: 126

Kev
Kev

Reputation: 5442

You can't import conditionally, but you can do the opposite: export something conditionally. It depends on your use case, so this work around might not be for you.

You can do:

api.js

import mockAPI from './mockAPI'
import realAPI from './realAPI'

const exportedAPI = shouldUseMock ? mockAPI : realAPI
export default exportedAPI

apiConsumer.js

import API from './api'
...

I use that to mock analytics libs like mixpanel, etc... because I can't have multiple builds or our frontend currently. Not the most elegant, but works. I just have a few 'if' here and there depending on the environment because in the case of mixpanel, it needs initialization.

Upvotes: 94

ericsoco
ericsoco

Reputation: 26253

Looks like the answer is that, as of now, you can't.

http://exploringjs.com/es6/ch_modules.html#sec_module-loader-api

I think the intent is to enable static analysis as much as possible, and conditionally imported modules break that. Also worth mentioning -- I'm using Babel, and I'm guessing that System is not supported by Babel because the module loader API didn't become an ES6 standard.

Upvotes: 10

Related Questions