339

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.

ericsoco
  • 24,913
  • 29
  • 97
  • 127
  • 11
    My use case: I want to make it easy to have an optional dependency. If the dep is not needed, the user removes it from `package.json`; my `gulpfile` then checks if that dependency exists before performing some build steps. – ericsoco Apr 04 '16 at 16:16
  • 3
    Another use case: for testing purposes. I am using `webpack` and `babel` to transpile es6 to es5. Projects like `webpack-rewire` and similar are not to help here - https://github.com/jhnns/rewire-webpack/issues/12 . One way to set the test doubles OR to remove problematic dependencies could be the conditional import. – Amio.io Sep 05 '16 at 06:58
  • 4
    +1. Being able to use a module in multiple environments where dependencies may or may not work is critical, particularly when modules may refer to dependencies that would only work in the browser (e.g. where `webpack` is used to convert stylesheets into modules that insert the relevant styles into the DOM when they're imported) but the module also needs to run outside of the browser (e.g. for unit testing). – Jules Jun 30 '17 at 09:22
  • If this `(condition)` can be resolved at build time then different preprocessed versions of the product can be prepared and the condition removed. E.g., `(condition)` is meant to distinguish front end (browser) vs back end (common js). Then the condition statement becomes unnecessary. – Craig Hicks Jul 22 '19 at 16:37

17 Answers17

261

We do have dynamic imports proposal now with ECMA. This is in stage 3. 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. Resolution of promise is expected to have the module. The proposal also have other features like multiple dynamic imports, default imports, js file import etc. You can find more information about dynamic imports here.

thecodejack
  • 12,689
  • 10
  • 44
  • 59
  • 23
    Finally, a real, ES6 answer! Thanks @thecodejack. Actually at stage 3 as of this writing, according to that article now. – ericsoco Oct 09 '17 at 04:37
  • 9
    or if you have just named exports you can destructure: `if (condition) { import('something') .then(({ somethingExported }) => { console.log(somethingExported); }); }` – IVN Mar 23 '18 at 14:56
  • 4
    on Firefox and while running `npm run build` I still get the error: `SyntaxError: ... 'import' and 'export' may only appear at the top level` – ste May 30 '18 at 07:27
  • this fails in testing tho, anyone have ideas? – stackjlei Apr 11 '19 at 16:51
  • Is there an option that does not involve Promises? What if your code cannot be async, but you still want to check for (and use) optional dependencies (e.g. something that's NOT a visual component, an add-on, ...)? – Flávio Lisbôa Apr 15 '19 at 03:29
  • you can use `async await` but still internally it is asynchronous only – thecodejack Apr 23 '19 at 11:26
  • Update: Implemented in Chrome 63. – Velojet May 17 '19 at 01:49
  • 3
    @stackjlei: This feature is not yet part of the JavaScript standard, it is just a stage 3 proposal! However it is already implemented in many newer browsers, see https://caniuse.com/#feat=es6-module-dynamic-import. – Konrad Höffner Jun 06 '19 at 08:22
  • 2
    That conditional dynamic import function doesn't have the fine grained ability to import only particular elements that "import X from Y" has. In fact that fine grained ability could be even more important in dynamic loading (as opposed to preprocess bundling) – Craig Hicks Jul 22 '19 at 16:13
  • 1
    Note that there is no value in "import X from Y" for dynamic imports. The parser loads in the entire module already anyway, so exporting as a single namespace when you just need a subset is a matter of deferencing: `import("thing").then({only, these, things} => { ... })`. – Mike 'Pomax' Kamermans Oct 10 '20 at 22:57
  • one gotcha I have come across though naive one be it is what may cause some headaches. Suppose one is conditional loading for side effects e.g. plugins through dynamic import function with will execute at run time as compared to import which executes at compile time. Any subsequent call to functions within that module and are out side of then per say. script will halt as there is no sure shot way of knowing as to when the loading has finished. import() is non blocking. in such case kev answer becomes relevant. – Syed Jan 29 '21 at 01:56
  • come to think of it ... I guess we are getting there in next few years imports might come close to what php did with include .... there is such a thing as globalThis now days. This is the point where Mike comment becomes relevant. key difference is however "import X from Y" although all A-Z are loaded in import only X is exposed to the module scope. – Syed Jan 29 '21 at 02:02
  • Technically it's ES11 – Dylan Maxey May 07 '22 at 06:39
  • Kindly note that if the thing that you are importing is a `default` export, you must treat this 1️⃣ using `.default`. View [more ℹ️ info on 2ality](https://2ality.com/2017/01/import-operator.html#accessing-default-exports). – CodeFinity Jun 24 '22 at 23:40
121

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();
}
BaptWaels
  • 1,646
  • 2
  • 14
  • 17
  • 1
    I think something and other variables are declsred using const which is block scoped, so the second if condition will throw that something is not defined – Mohammed Essehemy Apr 09 '18 at 12:49
  • Would be better to use let and declare the two variables outside the block instead of using 'var' and avoiding the block scope altogether. – Vorcan Apr 12 '18 at 07:12
  • Does hoisting affect anything in this case? I've run into some problems where hoisting has meant that I've unanticipatedly imported a library when following a pattern close to this if memory serves. – Thomas Oct 11 '18 at 13:18
  • 34
    It needs to be pointed out that `require()` is not part of standard JavaScript - it's a built-in function in Node.js, so only useful in that environment. The OP gives no indication of working with Node.js. – Velojet May 16 '19 at 22:37
  • 2
    2020 edit: both static and dynamic imports are now part of the standard JS offering. – Mike 'Pomax' Kamermans Oct 10 '20 at 23:01
88

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.

Kev
  • 5,049
  • 5
  • 32
  • 53
  • 64
    This solution causes unwanted modules be loaded, so not an optimal solution, I think. – ismailarilik Nov 11 '17 at 07:24
  • 7
    As stated in the answer, this is a work-around. At that time, there was simply no solution. ES6 imports are not dynamic, this is by design. The ES6 dynamic import function proposal, which is described in the currently accepted answer, can do it. JS is evolving :) – Kev Nov 12 '17 at 09:33
  • I think it's really nice, because I want the import at different places. Afterwards you can delete / comment the mockAPI – Johan Hoeksma Sep 08 '21 at 10:40
  • This is quite elegant. – crtag Apr 05 '22 at 01:04
  • @ismailarilik Yep. And I can confirm that the bundler isn't able to tree-shake or code-split/lazy-load this (well, at least at the time of this writing ;-D). – Glenn Mohammad Nov 18 '22 at 00:22
65

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

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Leonardo Raele
  • 2,400
  • 2
  • 28
  • 32
  • 1
    But how about re-export? – HellBaby Aug 18 '21 at 12:11
  • @HellBaby It doesn't make sense to dynamically export something from a module, since module code is only run once, when the module is imported. What you probably want is to export a function that either gets a callback function as argument or returns a promise, this way you can notify the client when something is done after some time. – Leonardo Raele Jul 03 '22 at 01:43
  • Amazing man, you made me realize what was my problem when importing `msw` dynamically. – SalahAdDin Dec 13 '22 at 16:26
  • 7
    ❤️ for keeping StackOverflow up to date. – mikemaccana Feb 20 '23 at 21:40
10

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.

ericsoco
  • 24,913
  • 29
  • 97
  • 127
7

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(...);
}
Solo
  • 6,687
  • 7
  • 35
  • 67
  • 2
    But `import` returns a promise. – newguy Jun 05 '20 at 08:35
  • @newguy Webpack replaces node-like environment variables (i.e. `process.env.SOMETHING`) at build time. That means if the environment variable is not "true" in the example above, webpack will remote the `if` statement, since it basically becomes dead code. This webpack behavior doesn't have anything at all to do with imports. – Leonardo Raele Mar 12 '21 at 20:39
  • Let me clarify: It is important that the "process.env.SOMETHING === 'true'" condition be written directly into the "if". It cannot be put into a variable, otherwise "webpack" will not be able to check if this block of code is unreachable – Kirill May 30 '23 at 09:30
7

Import and Export Conditionally in JS

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

export default value
4

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]);
Shoaib Nawaz
  • 2,302
  • 4
  • 29
  • 38
  • 13
    It needs to be pointed out that `require()` is not part of standard JavaScript - it's a built-in function in Node.js, so only useful in that environment. The OP gives no indication of working with Node.js. – Velojet May 16 '19 at 22:38
2

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

Elliot Ledger
  • 451
  • 4
  • 8
  • 7
    It needs to be pointed out that `require()` is not part of standard JavaScript - it's a built-in function in Node.js, so only useful in that environment. The OP gives no indication of working with Node.js. – Velojet May 16 '19 at 22:39
1

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

if (typeof __CLI__ !== 'undefined') {
  eval("require('fs');")
}
Liam
  • 27,717
  • 28
  • 128
  • 190
  • 4
    May anybody explain why this answer was downvoted? Is there any real drawbacks or it was just automatic negative reaction to evil keyword 'eval'? – Yuri Gor Feb 13 '19 at 07:41
  • 4
    Automatic downvote for using the hideous eval keyword. Stay away. – Tormod Haugene Feb 25 '19 at 16:29
  • 2
    Can you explain what is actually wrong with the use of `eval` here, @TormodHaugene? – Adam Barnes Apr 23 '19 at 11:54
  • MDN sums up quite a few reasons why [`eval` should not be used](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#Do_not_ever_use_eval!). In general: if you find the need to use `eval`, you are probably doing it wrong and should take a step back to consider your alternatives. There are probably some scenarios where using `eval` is correct, but you most likely have not encountered one of those situations. – Tormod Haugene Apr 23 '19 at 13:21
  • 5
    It needs to be pointed out that `require()` is not part of standard JavaScript - it's a built-in function in Node.js, so only useful in that environment. The OP gives no indication of working with Node.js. – Velojet May 16 '19 at 22:39
0

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

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

if(something) {
  something.doStuff();
}
bradley2w1dl
  • 81
  • 1
  • 2
  • 6
    It needs to be pointed out that `require()` is not part of standard JavaScript - it's a built-in function in Node.js, so only useful in that environment. The OP gives no indication of working with Node.js. – Velojet May 16 '19 at 22:39
0

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

Ashok R
  • 19,892
  • 8
  • 68
  • 68
0

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.

Aldee
  • 4,439
  • 10
  • 48
  • 71
0

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

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

Varun
  • 542
  • 1
  • 7
  • 17
0

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"
    }
}
Gustavo Vargas
  • 2,497
  • 2
  • 24
  • 32
0

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();
  });
}
Egg
  • 101
  • 1
0

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.

Kirill
  • 161
  • 2
  • 15