121

I've noticed that React can be imported like this:

import * as React from 'react';

...or like this:

import React from 'react';

The first imports everything in the react module (see: Import an entire module's contents)

The second imports only the default module export (see: Importing defaults)


It seems like the two approaches are different and fundamentally incompatible.

Why do they both work?


Please reference the source code and explain the mechanism...I'm interested in understanding how this works.


Update

This is not a duplicate of What is the difference between import * as react from 'react' vs import react from 'react'

That question was answered with general ES6 module information.

I am asking about the mechanism that makes the react module work like this. It seems to be related to "hacky" export mechanism in the source here but it's not clear how that enables both importing the entire module and just the default export into React and having both of those approaches work with transpiling JSX, etc.

Brian Adams
  • 43,011
  • 9
  • 113
  • 111
  • 1
    You asked `Why do they both work?` and I ask you, `Why wouldn't they work?` – Vencovsky Mar 21 '19 at 17:13
  • If importing just the default, and everything, gives you the same result, what do you think "everything" entails? Have you looked at the source you are importing? – Kevin B Mar 21 '19 at 17:13
  • `import * as React from 'react'; ` include `default`, I suppose. – Alex Mar 21 '19 at 17:17
  • 3
    In typescript you can specify in the `tsconfig.json` to `allowSyntheticDefaultImports`. This is what would let you `import React from 'react'`. Javascript/babel "cheats" here by letting you do that synthetic default import when it isn't actually there. The correct syntax for the way `React` is exported should be `import * as React from 'react'` – John Ruddell Mar 21 '19 at 17:19
  • 1
    @KevinB I've looked at the source, seems related to [this "hacky" line](https://github.com/facebook/react/blob/56035dac645af75c1a8aa8deba2bcf89b14a5aa4/packages/react/index.js#L14-L16) but it's not clear to me how it works – Brian Adams Mar 21 '19 at 17:33
  • @Vencovsky `Why wouldn't they work?`: because the `default` export would be accessed as `React.default` when importing **everything**, and would be accessed as `React` when importing **only** the default – Brian Adams Mar 21 '19 at 17:35
  • @AlexanderYakushev yes importing **everything** includes the default, but then it would be accessed differently (as `React.default` instead of just `React`) – Brian Adams Mar 21 '19 at 17:42
  • Does this answer your question? [What is the difference between import \* as react from 'react' vs import react from 'react'](https://stackoverflow.com/questions/54585763/what-is-the-difference-between-import-as-react-from-react-vs-import-react-fr) – You Nguyen Dec 24 '20 at 06:32

2 Answers2

79

TL;DR

Indeed ES import statements import default and import * are not the same thing, the fact that they behave the same in this case is a combination of how React authors chose to publish the library and compatibility layers in TypeScript (using esModuleInterop) or Babel and your bundler to make them "just work". It probably shouldn't work according to ES6 spec, but today we are still working in an era where JS modules are a mess, so tools like Babel, TypeScript, Webpack, etc try to normalize behavior.

More details:

React is not an ES6 library. If you look at the source code you see this in index.js:

const React = require('./src/React');

// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest.
module.exports = React.default || React;

(Note the comment, even in React source code they struggle with ES6 default export compatibility.)

The module.exports = syntax is CommonJS (NodeJS). A browser would not understand this. This is why we use bundlers like Webpack, Rollup, or Parcel. They understand all kinds of module syntax and produce bundles that should work in the browser.

But even though React is not an ES library, both TypeScript and Babel let you import it as if it is (using import syntax, rather than require(), etc), but there are differences between CJS and ES that have to be resolved. One of them is the fact that export = can give you things that ES has no spec-compliant way to import, like a function or a class as the module. To work around these incompatibilities Babel has for awhile allowed you to import CJS modules as if they were exporting something by default, or import as a namespace. TypeScript for awhile didn't do this, but more recently added that as an option under esModuleInterop. So now both Babel and TypeScript can pretty consistently allow a CJS module to be imported using default or namespace ES imports.

With TypeScript it also depends on how the type-definitions for the library are actually defined. I won't get into that, but you can imagine situations where thanks to transpilers and bundlers a particular import works at runtime, but TypeScript doesn't compile without errors.

Another thing worth mentioning is that if you look at the built code for React there is a UMD module version as well as the CJS version. The UMD version includes some gnarly runtime code to try to make it work in any module environment, including the browser. It's mainly for use if you want to just include React at runtime (ie you don't use a bundler). Example.

Confusing? Yeah, I think so. :)

Aaron Beall
  • 49,769
  • 26
  • 85
  • 103
  • 3
    The code piece referenced here is no longer in the index.js file. is there an update you could give us here @Aaron ? – Joe Lloyd Apr 09 '20 at 10:00
  • 1
    The code was changed in ["Export React as Named Exports instead of CommonJS"](https://github.com/facebook/react/pull/18106) pull request. – c0m1t Feb 06 '23 at 22:37
35

You most likely have "allowSyntheticDefaultImports": true, set in your tsconfig.json, which essentially shuts the compiler up about default imports it thinks are invalid. Typescript added esModuleInterop which does essentially what babel does for module loading.

This allows you to use ES6 default imports even when the source code you're importing doesn't export anything as default

Typescript is strict (follows the rules) when it comes to this, which is why they require you to import * as React from 'react'. Or requires you to tell it to allow synthetic default imports in its base config.

More On That Here

John Ruddell
  • 25,283
  • 6
  • 57
  • 86
  • 1
    Interesting, that could explain it for TypeScript, but it works for just JavaScript as well so there must be something else going on. You mentioned `Babel` in your comment above...could the `Babel` transpiler for JSX be doing something tricky? – Brian Adams Mar 21 '19 at 17:39
  • 1
    @brian-lives-outdoors Yes, babel essentially does this by default. [I believe its here](https://babeljs.io/docs/en/babel-plugin-transform-es2015-modules-commonjs). Also [this is a pretty good read](https://itnext.io/great-import-schism-typescript-confusion-around-imports-explained-d512fc6769c2) – John Ruddell Mar 21 '19 at 17:40
  • 3
    It's actually `esModuleInterop` that makes this work. Babel does something similar when handling module. Modules are kind of a mess, per ES6 spec `import default` and `import *` should not be the same thing, but often times they are interchangeable in transpilers and bundlers to avoid confusion or incompatibility. – Aaron Beall Mar 21 '19 at 18:00
  • 2
    [Great article](https://itnext.io/great-import-schism-typescript-confusion-around-imports-explained-d512fc6769c2) John, thanks. I'm going to read up on this before I accept but it looks like you and @Aaron are right...it's the transpilers that make it work – Brian Adams Mar 21 '19 at 18:28
  • 1
    @Aaron yep I mentioned that. `allowSyntheticDefaultImports` was the solution before `esModuleInterop` – John Ruddell Mar 21 '19 at 22:42