Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formalize top-level ES exports #11503

Open
gaearon opened this issue Nov 9, 2017 · 123 comments
Open

Formalize top-level ES exports #11503

gaearon opened this issue Nov 9, 2017 · 123 comments

Comments

@gaearon
Copy link
Collaborator

gaearon commented Nov 9, 2017

Currently we only ship CommonJS versions of all packages. However we might want to ship them as ESM in the future (#10021).

We can't quite easily do this because we haven't really decided on what top-level ES exports would look like from each package. For example, does react have a bunch of named exports, but also a default export called React? Should we encourage people to import * for better tree shaking? What about react-test-renderer/shallow that currently exports a class (and thus would start failing in Node were it converted to be a default export)?

@Andarist
Copy link
Contributor

Andarist commented Nov 9, 2017

Imho import * is a way to go, Im not opposed to having a default export too, but it shouldnt be used to reexport other stuff like in this example:

export const Component = ...
export default React
React.Component = Component

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 9, 2017

but it shouldnt be used to reexport other stuff like in this example:

Is there a technical reason why? (Aside from having two ways to do the same thing.)

My impression is that people who would import * (and not use the default) wouldn't have problems tree shaking since default would stay unused. But maybe I overestimate Rollup etc.

@Andarist
Copy link
Contributor

Andarist commented Nov 9, 2017

That questions can be probably best answered by @lukastaegert. Ain't sure if something has changed since #10021 (comment)

Also Rollup is not the only tree shaker out there, and while webpack's tree-shaking algorithm is worse than the one in rollup, it's usage is probably way higher than rollup's (both tools do excellent jobs ofc, I don't want to offend anyone, just stating facts) and if we can (as the community) help both tools at once we should do so whenever we can.

@jquense
Copy link
Contributor

jquense commented Nov 9, 2017

is tree-shaking going to do anything in React's case, given that everything is preprocessed into a single flat bundle? I wonder what the primary import style is for React, personally i tend to treat it like a default export e.g. React.Component, React.Children but occasionally do the named thing with cloneElement

@lukastaegert
Copy link

lukastaegert commented Nov 10, 2017

As @gaearon already stated elsewhere, size improvements in case of react are expected to be minimal. Nevertheless, there ARE advantages:

  • React.Children might probably be removed in some cases (so I heard 😉)
  • React itself can be hoisted into the top scope by module bundlers that support this. This could again remove quite a few bytes and might also grant an oh-so-slight performance improvement. The main improvement would lie in the fact that there does not need to be another variable that references React.Component for every module but just one that is shared everywhere (this is how rollup usually does it). Also, though this is just me guessing, this might reduce the chance of webpack's ModuleConcatenationPlugin bailing out
  • Static analysis for react is easier not only for module bundlers but also for e.g. IDEs and other tools. Many such tools already do a reasonable job at this for CJS modules but in the end, there is a lot of guessing involved on their side. With ES6 modules, analysis is a no-brainer.

As for the kind of exports, of course only named export really provide the benefit of easy tree-shaking (unless you use GCC which might be able to do a little more in its aggressive move and maybe the latest rollup if you are really lucky). The question if you provide a default export as well is more difficult to decide:

  • PRO: Painless migration for existing ES6 code bases (e.g. what @jquense describes)
  • CON: Since everything is attached to a common object, once this object is included, all its keys are included at once which again defeats any attempts at tree-shaking. Even GCC might have a hard time here.

As a two-version migration strategy, you might add a default export in the next version for compatibility purposes which is declared deprecated (it might even display a warning via a getter etc.) and then remove it in a later version.

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 13, 2017

This is also an interesting case: #11526. While monkeypatching for testing is a bit shady, we'll want to be conscious about breaking this (or having a workaround for it).

@Rich-Harris
Copy link
Contributor

Came here via this Twitter conversation. For me, there's a clear correct answer to this question: React and ReactDOM should only export named exports. They're not objects that contain state, or that other libraries can mutate or attach properties to (#11526 notwithstanding) — the only reason they exist is as a place to 'put' Component, createElement and so on. In other words, namespaces, which should be imported as such.

(It also makes life easier for bundlers, but that's neither here nor there.)

Of course, that does present a breaking change for people currently using a default import and transpiling. @lukastaegert probably has the right idea here, using accessors to print deprecation warnings. These could be removed in version 17, perhaps?

I don't have a ready-made suggestion for #11526 though. Perhaps shipping ESM would have wait for v17 for that reason anyway, in which case there'd be no need to worry about deprecation warnings.

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 23, 2017

People have really come to like

import React, { Component } from 'react'

so convincing them to give it up might be difficult.

I guess this is not too bad, even if a bit odd:

import * as React from 'react';
import { Component } from 'react';

To clarify, we need React to be in scope (in this case, as a namespace) because JSX transpiles to React.createElement(). We could break JSX and say it depends on global jsx() function instead. Then imports would look like:

import {jsx, Component} from 'react';

which is maybe okay but a huge change. This would also mean React UMD builds now need to set window.jsx too.

Why am I suggesting jsx instead of createElement? Well, createElement is already overloaded (document.createElement) and while it's okay with React. qualifier, without it claiming it on the global is just too much. Tbh I’m not super excited about either of these options, and think this would probably be the best middle ground:

import * as React from 'react';
import { Component } from 'react';

and keep JSX transpiling to React.createElement by default.

@Rich-Harris
Copy link
Contributor

Confession: I always found it slightly odd that you have to explicitly import React in order to use JSX, even though you're not actually using that identifier anywhere. Perhaps in future, transpilers could insert import * as React from 'react' (configurable for the sake of Preact etc) on encountering JSX, if it doesn't already exist? That way you'd only need to do this...

import { Component } from 'react';

...and the namespace import would be taken care of automatically.

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 23, 2017

In a distant future, maybe. For now we need to make sure transpilers work with other module systems (CommonJS or globals). Making this configurable is also a hurdle, and further splits the community.

@Andarist
Copy link
Contributor

What @Rich-Harris suggested (inserting a specific import when jsx is used) is easily done by transpilers plugin. The community would have to upgrade their babel-plugin-transform-react-jsx and that's it. And of course even existing setups would still work if only one adds import * as React from 'react'; to the file.

Of course we need to consider other module systems, but it doesn't seem like a hard problem to solve. Are there any specific gotchas in mind?

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 23, 2017

Of course we need to consider other module systems, but it doesn't seem like a hard problem to solve. Are there any specific gotchas in mind?

I don’t know, what is your specific suggestion as to how to handle it? Would what the default be for Babel JSX plugin?

@jamiewinder
Copy link

People have really come to like

import React, { Component } from 'react'

What people? Come forth so that I may mock thee.

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 23, 2017

I did that a lot 🙂 Pretty sure I've seen this in other places too.

@Andarist
Copy link
Contributor

Default is at the moment React.createElement and it would pretty much stay the same. The only problem is that it assumes a global now (or already available in the scope).

I think as es modules are basically the standard way (although not yet adopted by all) of doing modules, it is reasonable to assume majority is (or should) use it. Vast majority already uses various build step tools to create their bundles - which is even more true in this discussion because we are talking about transpiling jsx syntax. Changing the default behaviour of the jsx plugin to auto insertion of React.createElement into the scope is imho reasonable thing to do. We are at the perfect time for this change with babel@7 coming soon (-ish). With recent addition of babel-helper-module-imports it is also easier than ever to insert the right type of the import (es/cjs) to the file.

Having this configurable to bail out to today's behaviour (assuming present in scope) seems really like a minor change in configuration needed for a minority of users and an improvement (sure, not a big one - but still) for majority.

@kzc
Copy link

kzc commented Dec 3, 2017

Should we encourage people to import * for better tree shaking?

Thanks to @alexlamsl uglify-es has eliminated the export default penalty in common scenarios:

$ cat mod.js 
export default {
	foo: 1,
	bar: 2,
	square: (x) => x * x,
	cube: (x) => x * x * x,
};
$ cat main.js 
import mod from './mod.js'
console.log(mod.foo, mod.cube(mod.bar));
$ rollup main.js -f es --silent | tee bundle.js
var mod = {
	foo: 1,
	bar: 2,
	square: (x) => x * x,
	cube: (x) => x * x * x,
};

console.log(mod.foo, mod.cube(mod.bar));
$ uglifyjs -V
uglify-es 3.2.1
$ cat bundle.js | uglifyjs --toplevel -bc
var mod_foo = 1, mod_bar = 2, mod_cube = x => x * x * x;

console.log(mod_foo, mod_cube(mod_bar));
$ cat bundle.js | uglifyjs --toplevel -mc passes=3
console.log(1,8);

@Andarist
Copy link
Contributor

Andarist commented Dec 3, 2017

wow, that's great new 👏 is uglify-es considered to be stable now? I recall you mentioning few months back that it isn't there quite yet, but I can remember that incorrectly, so ain't sure.

Anyway - that's all and nice in a rollup world, but considering that React is bundled mostly in apps and those use mostly webpack which does not do scope hoisting by default, I'd still say that exporting an object as default should be avoided to aid other tools than uglisy-es+rollup in their efforts to produce smaller bundle sizes. Also for me it is semantically better to avoid this - what libs actually do in such cases is providing a namespace and it is better represented when using import * as Namespace from 'namespace'

@kzc
Copy link

kzc commented Dec 3, 2017

is uglify-es considered to be stable now?

As stable as anything else in the JS ecosystem. Over 500K downloads per week.

that's all and nice in a rollup world, but considering that React is bundled mostly in apps and those use mostly webpack which does not do scope hoisting by default

Anyway, it's an option. Webpack defaults are not ideal anyway - you have to use ModuleConcatenationPlugin as you know.

@lukastaegert
Copy link

lukastaegert commented Dec 4, 2017

Adding a few cents here:

  • I totally agree with @Rich-Harris that semantically, named exports are the right choice
  • I really do not like either import React from 'react' or import * as React from 'react' just to be able to use JSX syntax. In my eyes, this design is clearly violating the Interface Segregation Principle in that it forces users to import all of React just to be able to use the createElement part (though admittedly with a namespace export, a bundler like Rollup will strip out the unneeded exports again)

So if we are at a point where we might make breaking-change decisions, I would advise to change this so that JSX depends on a single (global or imported) function. I would have called it createJSXElement(), which in my opinion describes it even better than createElement() and no longer needs the React context to make sense. But in a world where every byte counts, jsx() is probably ok, too.

This would also at last decouple JSX from React in a way such that other libraries can choose to support JSX by using the same transformation and supplying a different jsx function. Of course you have a lot of responsibility here guiding countless established applications through such a transformation but from an architectural point of view, this is where I think React and JSX should be heading. Using Babel to do the heavy lifting of such a transformation sounds like a great idea to me!

@Andarist
Copy link
Contributor

Andarist commented Dec 4, 2017

Personally I do not see much gain in migrating to jsx helper as the default IMHO for the babel plugin should be importing it from the react package, so the name of the actual helper doesn't really matter - the rest is just matter of having it configurable.

@NMinhNguyen
Copy link
Contributor

NMinhNguyen commented Dec 12, 2017

This is probably slightly tangential to the main discussion, but I'm curious how well ES modules work with checking process.env.NODE_ENV to conditionally export dev/prod bundles? For example,

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}

I may be missing something obvious here, but I'm struggling to see how to translate this pattern into ES modules?

@milesj
Copy link
Contributor

milesj commented Dec 12, 2017

@NMinhNguyen Conditional exports aren't possible with ES modules.

@Andarist
Copy link
Contributor

process.env.NODE_ENV checks can be at more granular (code) level though, ready to be replaced by the bundler with appropriate values.

@NMinhNguyen
Copy link
Contributor

NMinhNguyen commented Dec 12, 2017

@Andarist @milesj Thanks for confirming my suspicion :)

process.env.NODE_ENV checks can be at more granular (code) level though, ready to be replaced by the bundler with appropriate values.

From the React 16 blog post I thought that the process.env.NODE_ENV checks were pulled out to the very top on purpose (as opposed to them being more granular, which is what they are in the source, if I'm not mistaken), to help performance in Node.js?

Better server-side rendering

React 16 includes a completely rewritten server renderer. It's really fast. It supports streaming, so you can start sending bytes to the client faster. And thanks to a new packaging strategy that compiles away process.env checks (Believe it or not, reading process.env in Node is really slow!), you no longer need to bundle React to get good server-rendering performance.

Like, I'm not sure how one could use the module field in package.json and differentiate between dev/prod for ESM while keeping ES bundles flat and not affecting Node.js perf

@Andarist
Copy link
Contributor

Like, I'm not sure how one could use the module field in package.json and differentiate between dev/prod for ESM while keeping ES bundles flat and not affecting Node.js perf

This for sure is a drawback, because there is no standard way at the moment for doing this. OTOH it's just a matter of tooling, it is possible (and it's rather easy) to compile this in build steps of your application even today. Ofc it would be easier if package could expose dev/prod builds and the resolver would just know which one to pick, but maybe that's just a matter of pushing this idea to tooling authors.

@juliuskovac
Copy link

juliuskovac commented Jan 5, 2021

With all the respect, wrapper doesn't sound like cheating, it is cheating. Tree shaking and other tools to analyze code which know only about ES6 modules semantics won't fully work with it ever. If there will be major release of React, I think it will be more beneficial for community to go with correct ES6 modules and named-only exports, that's what versioning is about to do breaking changes if they are really necessary. If there is problem with tooling (to bundle two versions or other issue) it should be probably fixed on tooling side.

@ljharb
Copy link
Contributor

ljharb commented Jan 5, 2021

@juliuskovac tree shaking works equally well with CJS, it's just not enabled by default in the popular bundlers, so they could already handle things without any ESM whatsoever if it was important to do so. The problem that needs fixing on the tooling side is "treeshaking often only works with ESM by default".

@lukastaegert
Copy link

I assure you, it is not a question of "defaults". It is not impossible, but at least for Rollup, there is still some engineering effort necessary to safely track usage of object properties as would be required for CommonJS tree-shaking. And beyond tree-shaking, scope-hoisting is another optimization you forego without ES modules. In short it means you have a lot more property accesses with the incurred runtime penalty in a CommonJS setting. Rollup is actively trying to reduce these, but it requires libraries to be sufficiently "well-behaved". In short, it is quite hard to produce similar quality output from CommonJS modules as opposed to ES modules.

@ljharb
Copy link
Contributor

ljharb commented Jan 5, 2021

Since dynamic imports exist, well-behaved libraries are required anyways.

@hronro
Copy link

hronro commented Jun 10, 2021

The plan for React 18 is finally announced, but I'm a little disappointed that there is no mention of ES module support at all.
ES module support is a feature request for a long time, and even this issue has been opened for about 4 years, I really hope the new major release (React 18) could have the feature implemented.

@bvaughn
Copy link
Contributor

bvaughn commented Jun 10, 2021

One of our main goals for the 18.0 release is to remove as many upgrade obstacles as possible so that people can migrate and began taking advantage of the new features and fixes. ES modules would be a breaking change (so they would compete with this goal). They are still important to us though, and we hope to make the change sometime in the near future, just not as part of 18.0.

@wereHamster
Copy link

There is currently much confusion about whether to import React from or import * as React from. Both work, currently, but that's only accidental due to how ESM imports of CommonJS modules are treated by bundlers.

I'd really appreciate an official stance by the React developers as to which form will be supported once React starts providing native ESM, so that we can start educating people now, update examples, blog posts, provide linter rules, emit bundler warnings etc now, rather than wait with it until the ESM support finally ships…

@milesj
Copy link
Contributor

milesj commented Jun 11, 2021

Pretty sure the path forward is named imports only, so import { useState } from 'react';. The default import is no longer required if you are using the new automatic JSX transform.

At work, we completely removed the default import and used the new transformer without any issues. It's much much cleaner.

@bvaughn
Copy link
Contributor

bvaughn commented Jun 11, 2021

@milesj is correct. The "React" import is no longer required for JSX usage, thanks to the efforts of @lunaruan last year (via #16626 and a few other related commits)

@hronro
Copy link

hronro commented Nov 10, 2021

I'm a little bit confused about the roadmap now. If ES module will not come with v18.0, will it come with v18.x or v19.0? (or ... even later?)

I'm not sure if React.js follows semantic versioning. If it does, it's actually OK to introduce a breaking change in a major release.

If React.js doesn't follow semantic versioning and finally supports ES module in v18.x, it's still weird that React.js does not introduce breaking changes in a major version but introduces them in a minor version.

If React.js finally supports ES module in v19.0, OK, maybe we have to wait another 4 years for the ES module support.

@thientran1707
Copy link

may I know if we have any status for the ESM module support from React?

@wereHamster
Copy link

If #18102 is to be believed, then the react team has decided on ESM exports… namely on using named exports instead of a default export. So… can this be closed?

@junyin-ms
Copy link

ES modules have been supported in major browsers for over 5 years now. When would an official ESM version of React be finally published?

@GabrielDelepine
Copy link

When would an official ESM version of React be finally published?

If the transition to ESM is not made properly, it will be a terrible mess. I am like you, I can't wait but I definitely prefer the team to take enough time to ship something great. Don't underestimate the amount of work, it's huge because they are so many implications

Until then, I am loading react and ReactDOM as a UMD global in my ESM repos and it's doing the job. Everything stays typed out of the box

package.json

{
  "type": "module"
}

tsconfig.json (Typescript 5.0)

{
  "compilerOptions": {
    "allowUmdGlobalAccess": true,
    "jsx": "react",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "resolveJsonModule": true,
    "strict": true,
    "target": "ES2022",
    "verbatimModuleSyntax": true
  }
}

index.html

    <script type="importmap">
      {
        "imports": {
               <====== list your ESM dependencies here
        }
      }
    </script>
    <!-- of course, only expose the desired files on production, not your complete node_modules/ folder -->
    <script src="../node_modules/react/umd/react.development.js"></script>
    <script src="../node_modules/react-dom/umd/react-dom.development.js"></script>
    <script type="module" src="./index.js"></script>

Screenshot 2023-04-28 at 19 08 45

@xiaoqiang1999
Copy link

Please support ESM as soon as possible, it is useful, thanks.
For example, Vue.js supported ESM long ago.

It is currently available for temporary use https://esm.sh/ To get the ESM package for React.

@Starcounter-Jack
Copy link

Starcounter-Jack commented Sep 24, 2023

I second the suggestion to use https://esm.sh/react@18 as they have provided a proper modern ESM distribution of React for a very long time. Just remember to use the import-map in your html for the "react". It tells what version of react the bare import should resolve to (just as in package.json).

No need for bundlers or arcane legacy module stuff. Bundling is for optimisation and for legacy browser support and should provide compatibility with ESM and not fight against it.

IMHO, a bundler should no longer be a development time dependency in 2023.

Just do:

import React from "react" 

@jonkoops
Copy link

jonkoops commented Nov 15, 2023

So our current thinking is that when we go ESM, we might want to try go ESM all the way. No CJS at all — or separated in a compat legacy package. This won't happen in React 17 and is unlikely in 18, but is plausible to try in React 19.

@gaearon Now that the new JSX transform has been out for a while, there should no longer be a blocker right? Perhaps the time is right to do this with React 19. How would you feel if I drafted up a renewed proposal along the lines of reactjs/rfcs#38?

@luzzif
Copy link

luzzif commented Dec 7, 2023

Guys please, is there any update on this?

@alshdavid
Copy link

alshdavid commented Mar 1, 2024

Bundling is for optimisation [..] IMHO, a bundler should no longer be a development time dependency in 2023.

There will always be need for a build step for sources as shipping sources directly cannot be as efficient as a tool designed to organize sources into an optimized output. Bundles are reasonable as they can take advantage of compression, dead code elimination, and bundlers/build systems can help handle consuming invalid sources (like consuming TypeScript, importing css, importing images, markdown, etc).

should provide compatibility with ESM and not fight against it.

It's not about fighting it, bundlers will remain compatible with it, it's just that CJS packages (React) cause bundlers to de-opt the output.

An example of such a de-opt is that - in the case of an ESM export like this:

export const createElement = () => {}

A bundler knows this value cannot be reassigned and therefore generates a direct property assignment within the isolated module scope

modules['react'] = (bundler_require, module) => {
  module.createElement = () => {}
}

However with CJS, a property assignments can be done internally within the module, externally by other modules, synchronously within a nested scope, etc - this means we can never guarantee that the value remains unchanged.

module.exports.foo = 'something'

module.exports.bar = function() {
  module.exports.foo = 'something else' 
}

So bundlers have to use getters/setters to "proxy" onto cjs properties

modules['react'] = (bundler_require, module) => {
  let foo_unique_id_1 = 'something'
  let bar_unique_id_2 = function() {
    foo_unique_id_1 = 'something else' 
  }
  Object.defineProperty(module, 'foo', { get: () => foo_unique_id_1, set: (v) => foo_unique_id_1 = v })
  Object.defineProperty(module, 'bar', { get: () => bar_unique_id_2, set: (v) => bar_unique_id_2 = v })

  // Synthetic default exports
  Object.defineProperty(module.default, 'foo', { get: () => foo_unique_id_1, set: (v) => foo_unique_id_1 = v })
  Object.defineProperty(module.default, 'bar', { get: () => bar_unique_id_2, set: (v) => bar_unique_id_2 = v })
}

Getters are 10x slower than direct property accesses and that adds up in a big project with lots of React in it.

Please distribute a copy of React as unminified ESM and let the bundlers do the heavy lifting of optimizing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests