react
react copied to clipboard
Formalize top-level ES exports
Currently we only ship CommonJS versions of all packages. However we might want to ship them as ESM in the future (https://github.com/facebook/react/issues/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)?
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
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.
That questions can be probably best answered by @lukastaegert. Ain't sure if something has changed since https://github.com/facebook/react/issues/10021#issuecomment-335128611
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.
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
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.Componentfor 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.
This is also an interesting case: https://github.com/facebook/react/issues/11526. While monkeypatching for testing is a bit shady, we'll want to be conscious about breaking this (or having a workaround for it).
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.
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.
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.
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.
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?
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?
People have really come to like
import React, { Component } from 'react'
What people? Come forth so that I may mock thee.
I did that a lot 🙂 Pretty sure I've seen this in other places too.
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.
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);
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'
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.
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'orimport * 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 thecreateElementpart (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!
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.
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,
https://github.com/facebook/react/blob/d9c1dbd61772f8f8ab0cdf389e70463d704c480b/packages/react/npm/index.js#L3-L7
I may be missing something obvious here, but I'm struggling to see how to translate this pattern into ES modules?
@NMinhNguyen Conditional exports aren't possible with ES modules.
process.env.NODE_ENV checks can be at more granular (code) level though, ready to be replaced by the bundler with appropriate values.
@Andarist @milesj Thanks for confirming my suspicion :)
process.env.NODE_ENVchecks 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.envchecks (Believe it or not, readingprocess.envin 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
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.
For class:
import Component from 'react/Component'
class MyButton extends Component{
constructor(){
this.state = {}
}
render() {
return <button> Button <Button>
}
}
Where transform will use super.createElement() to transform to jsx or use static Component.createElement().
For stateless components:
import jsx from 'react/jsx'
const MyButton = () => jsx`<button> Button <Button>`;
it is maybe possible to use tagged template literal?
Node hopefully accept this PR https://github.com/nodejs/node/pull/18392
We agree here with @Rich-Harris.
Just dropping a comment on this thread which hasn't really been mentioned specifically.
I’m in a situation where I’m not using a bundler at all and just want to import react and various components for use natively through the browser (<script type="module" src="...">), i.e.
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import ReactDOM from “https://unpkg.com/[email protected]/umd/react-dom.development.js”;
ReactDOM.render(
React.createElement(...),
document.getElementById('root')
);
From what I can tell, this isn't possible today. Instead, I have to include the UMD version of react via a <script> tag from the CDN and then assume it’s presence on the window in any <script type="module"> module I write:
// myPage.html
<div id="myComponentRoot"></div>
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script type="module" src="/assets/scripts/components/MyComponent.js"></script>
// MyComponent.js
import AnotherComponent from "/assets/scripts/components/AnotherComponent.js";
window.ReactDOM.render(
window.React.createElement(AnotherComponent),
document.getElementById('root')
);
// AnotherComponent.js
export default class AnotherComponent extends window.React.Component {...}
Having a react import from a CDN would be fantastic. It would make prototyping in the browser very quick and easy while still being able to maintain separation of files. One thing I always felt I was sacrificing when using React without a bundler was the ability to separate components (and other utility functions, etc) by file. But now with browser support for native ES modules, I can write my React components in separate files and have the browser just consume them as they're written. Granted that's if I'm not using JSX, but even if I was using JSX, I could transpire all the files in place via a build step and all my imports would still work in the browser.
// /assets/scripts/entry.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import React from “https://unpkg.com/[email protected]/umd/react-dom.development.js”;
import RelatedPosts from "/assets/scripts/components/RelatedPosts.js";
ReactDOM.render(
React.createElement(RelatedPosts),
document.getElementById('root')
);
// /assets/scripts/components/RelatedPosts.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import ListItem from "/assets/scripts/components/ListItem.js"
export default class MyComponent extends React.Component {
componentDidMount() { /* fetch some data */ }
render() {
return React.createElement(
'ul',
{},
this.state.items.map(item => React.createElement(ListItem, { item: item })
)
}
}
// /assets/scripts/components/ListItem.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
export default function ListItem(props) {
return React.createElement('li', null, ...)
}
I'm sure some people would argue typing that CDN url all the time is a problem (a problem some people are trying to fix) but the tradeoffs are worth it to me. Changing/updating that url is a simple find/replace. For my use case, this outweighs the trouble of setting up a bundler.
If React had support for something like this, there would be no need for tooling. I'm just using the browser. I could ship code like this in a few personal projects which assume modern browsers and use react as a progressive enhancement on the page. What makes this fantastic is when I come back to the code base in 12 months, I don't have to change a bunch of tooling APIs or even have NPM as a package manager. I'm just using APIs from the browser, nothing else.
FWIW: if/when React does ship with support like this, I think it could be very valuable to show how you could use React like this in the docs, teaching that you can leverage React and its component model by separating each components logic via its own file and you don't need a bundler to do it, just use native <script type="module">, import React from a CDN (or your own local copy), and your off!”
Now, all of the modern browsers including mobile versions support ESM. ESM is no longer a future module system but a current defacto-standard.
Please be aware of not providing the standardized module is a critical problem, especially for a defacto-standard web library.
import * as React from 'react';
import * as ReactDOM from 'react-dom';
This is the typical code to apply React libraries, and the fact has been there are not actually libraries that can be imported, instead, 3rd party transpilers and bundlers emulate the import process.
It's been slightly justified not providing the real ESM since browsers had not supported the native ESM anyway, but obviously, the time is up, and now is the time to provide ESM as specified the typical sample code to import.