-
Notifications
You must be signed in to change notification settings - Fork 560
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
React.lazy() #64
React.lazy() #64
Conversation
Is the idea that the return value of |
In the scope of this proposal, yes.
Which downsides are you referring to?
I think the RFC goes into detail about why we specifically want access to the module object. This will be important for future work on the new server renderer. An "explicit" proposal that doesn't lose this future potential would be something like lazy(() => import('./Button'), Button => Button.default); At this point it's going to be so common though that it might as well be the default (pun intended) behavior. It's not just a convenience hack. It corresponds semantically to what "default" exports mean. |
the inability to use named exports, or potentially do any post-import transform/instrumentation
Sorry yeah, I can see how that would be useful, but it also feels premature? As you noted what the module object returned here even looks like is nebulous and undefined, even in Node for the SSR case. This limitation doesn't seem to really put react in a better position for the future since there is no reason why users can't do I would also add that it feels uncomfortable to me that React would want integrate so tightly into the module space, before there is a clear sense of what the standard there is. Folks do so. many. weird things in webpack, et al, to meet their needs that this is going to be a moving target for a looong time.
I think that's debatable, :) people assign a lot of different meaning to this stuff and i don't think that's wrong or that there is a clear "correct" usage for default vs named exports semantically |
(do I think this a really cool addition to the API!) |
The proposal mentions we could add named exports to this.
We're starting work on this now (both standard proposals and the new server renderer implementation) so I wouldn't say it's premature. This is going to become a very active area of work for us.
We'll document that you're supposed to pass the module object, and nothing else. For sure, some people will ignore it, but then adopting the new server renderer isn't forced upon them. People who follow the recommendation would have an easier path. Again, if this doesn't work out, we can surely relax it in the future. In fact that's what the original proposal was (it took any Promise) but then we decided to make it stricter. |
I think the return statement should have a capital 'C'? |
text/0000-lazy.md
Outdated
const Button = lazy(async () => { | ||
const Components = await import('./components'); | ||
// Resolve to named export: | ||
return components.Button; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small typo -> Components.Button
(needs capital C, or lowercase on line 69).
Edit: Looks like @mellogarrett beat me to it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think its line 71
I think that once some of the other pieces of this puzzle fall into place. I suspect that the syntactical overhead will strongly push people towards the default exports. Especially if we can even get rid of the call to “lazy” in the default export case. |
Nice (I'm already using it https://github.com/pomber/code-surfer-editor/blob/master/src/LazyCodeEditor.js#L4 😁). |
In the alternatives, it's listed as a possibility to:
Has there been any more public discussion on the naming? Outside of the context of this discussion, For example, the functions dealing with refs say so explicitly (createRef, forwardRef). Since this probably isn't used in too many places throughout a code base, I think the tradeoff of adding extra characters for the sake of explicitness might be worth considering. |
On the topic of default exports... https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html I think most of the React community uses default exports however many, following the practices above, do not use default exports and prefer named exports. |
The longer term vision includes making literally every component in the whole code base use this. So you can imagine that syntax overhead will be a big deal and familiarity counter balance any explicitness concerns. |
I have a hard time imagining how this will play nicely with bundlers and efficient code splitting...at the moment that would create a new bundle per file in webpack no? |
at the moment yes |
// Annoying and confusing: | ||
const Button = lazy(() => import('./Button').then(Button => Button.default)); | ||
// Named imports don't make this better: | ||
const Button = lazy(() => import('./Button').then(Button => Button.Button)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is better
const Button = lazy(() => import('./Button').then(module => module.Button));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for why it's not this way is explained just below. Did you get a chance to read it?
const Button = lazy(() => import('./Button').then(Button => Button.Button)); | ||
``` | ||
|
||
(Note this doesn't mean you're forced to use default exports for *all* your components. Even if you primarily use named exports, consider default exports to be "async entry points" into just the components you want to code split.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds inconsistency in modules style. The point of component code splitting to be invisible. You take any module, wrap it with lazy
and it becomes loadable. Converting to default
export is additional and wrong action. We don't need to define that module as asynchronous. Module should be only consumed as asynchronous in place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for this is explained just below.
|
||
```js | ||
// Not a part of this RFC but plausible in the future | ||
const Button = lazy(() => import('./components'), components => components.Button); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nice, but we need something right now. Would be good to add at least an ugly example.
const Button = lazy(() =>
import('./components').then(module => ({ default: module.Button }))
);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think maybe we’ll try to warn against that pattern since it will break future work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get. How this will break future work? And how are you gonna forbid this? It's just a promise with object. We need a solution now. Using default exports for async components is awful workaround.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you feel so strongly against writing a single line of code that does the export?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now default
exports are discouraged. There are a lot of reasons behind:
- like
allowSyntheticDefaultImports:false
in typescript - like autoimports in IDE
- like auto "naming" things
Why not to support basic "babel" interop, ie use .default
if _esModules
set, mimicking how "real"(babel/webpack) imports work?
I also prefer to have a default export on code splitting point, as some sort of an "interface", but it should not be the law.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Babel interop has made the ESM ecosystem very messy and complex (even though it was instrumental in getting people to adopt ESM — perhaps too early). We don’t wait to add more tooling dependency on this behavior because it’s super hard to fix properly.
I also prefer to have a default export on code splitting point, as some sort of an "interface", but it should not be the law.
See #64 (comment).
I might be able to see the utility in that if you have a massive team working on components in a distributed fashion, where what code is being rendered for any given user might be impossible to reason about. For an average sized site, though, the extra latency of loading each component bundle individually would surely outweigh whatever gain you have from a faster initial page load. Maybe in an HTTP/2 world? |
practical named exports example btw Material UI exports a lot of components as named exports, if it makes sense maybe it's worth including the extension now? I also seethe value in limiting new API's initially |
When rehydration is possible, there needs to be away to tell the client to eagerly load components that were loaded lazily on the server and/or prime the lazy components cache before the initial render. |
Trying to find a component in The promise that resolves to a component does not necessarily have to be a dynamic Also, why force the community to use default exports for all components? Named exports are also a valid option, especially if you want to create a "bundle" of dynamically loaded components. |
|
||
```js | ||
// Annoying and confusing: | ||
const Button = lazy(() => import('./Button').then(Button => Button.default)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does not have to be written like that. Could be
const Button = lazy(async () => (await import('./Button')).default);
or even
const Button = lazyDefault(() => import('./Button'));
or
const Button = lazy(() => import('./Button'), 'default');
or
const Button = lazy(() => import('./Button').then(_ => _.default));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It most definitely needs to be a function that returns a promise, so the last 3 examples are invalid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@milesj thx, corrected
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it'll need the intermediate function so the dynamic import isn't immediately resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, without the function it's a side-effect.
Can you explain how Suspense will be able to make use of the actual module record? |
Here's an example: https://codesandbox.io/s/yx95qp06v Suspense will provide a fallback option while it's waiting for the Promise to settle, once the the Promise resolves it will show the imported component. |
Example of getting a Button from Material UI: https://codesandbox.io/s/kwzkor2p8v |
@GarethSmall Were you replying to me in your first comment? If so, how would the behavior be different if Suspense couldn’t access the module record, just the component? |
@j-f1 Suspense doesn't access the module record, Suspense is expecting a component or at least the promise of a component. |
@GarethSmall that behavior is changing, that some of the point of the RFC here |
@j-f1 Do you mean the lazy function or the Suspense component? |
@GarethSmall I mean the |
@j-f1 Ahh okay, I apologize I mis-spoke from a lack of understanding, so what I was doing was a bad practice. Is this what you were talking about from the RFC?: The second part of the question is why don't we still allow passing something without a .default property?
We intentionally don't support this in the scope of this proposal. This gives React an opportunity to integrate more tightly with the module system on the server in the future. There are no proposed standards for this yet, but the new Suspense-capable React server renderer we'll soon be working on will benefit from having a chance to introspect the import() result directly (rather than just a component). For example, it could change a priority of a pending request for the Button.js module once it knows that a component of the Button type is going to be lazily rendered. We can't do this if we break the link between the module and the component. While this depends on future experimentation and standardization work, this design leaves more space for it. We can always remove this restriction later if it doesn't end up being beneficial. (That's also why the proposal doesn't support arbitrary Promises as component types.) |
Thanks for explaining; that makes a lot more sense. |
How does this interact with const Button = React.memo(
React.lazy(() =>
import("third-party-button")
)
); |
No. The argument to It works the other way around though. Which is consistent with what we say in |
(This is more of a question about |
I think you might be confusing the syntax (import is asynchronous) with a specific behavior (create a bundle per component). There's absolutely no reason why tools like webpack need to create a separate bundle per dynamic import. If they had access to usage statistics, they could instead be smart about bundling modules together in the most efficient way based on which are likely to be rendered together. Combined with a server rendering solution, modules could also be streamed in the order they're likely to be hydrated and used. The "longer term" vision Sebastian alluded to is not about the current webpack behavior. But about allowing a better bundling solution. |
Regarding named exports. I hear where you’re all coming from. If you disagree with our recommendation to use default exports for split points, you can use a workaround described in #64 (comment). You could even your own This RFC has a limited surface. It's targeted at the main use case we want to start handling (default imports). There is a door open for supporting named imports in future RFCs, but it is out of scope of this one. This one is intentionally very limited. We expect to understand this problem space more within the next several months and then we can revisit that discussion with a better understanding. I understand you want us to support your use case today. But we don’t have all the information yet, and if the solution is too broad, it’s very likely we’ll have to deprecate or make breaking changes to it in the next release. Nobody likes breaking changes. Therefore, for now we’d like to focus it on the parts we feel more confident in. I hope this makes sense, even if you disagree. Note: regardless of how |
@gaearon I can definitely imagine having everything lazy-loaded if the compiler were smart enough. It would be great to move that decision of where to code split out of the code-base itself and put it into the compiler's hands. I realize now that in order to give the compiler the flexibility to do that, you'd have to do exactly what @sebmarkbage said and wrap |
Yes. This is something we want to be able to do automatically, which is why we need the module information (rather than just the Promise to a component). Then the server could send this information to the client embedded in the small runtime. |
It's not clear how server-side rendering works. To be more concrete - when component got imported. But on server side, it's very opinionated:
So - when imports will work on a server? |
Supporting today's server renderer is out of scope of this proposal (as mentioned in the RFC). In the future Suspense-capable server side renderer (which is fully asynchronous and can stream chunks while waiting for data) my mental model is that dynamic imports would be somehow rewritten to be synchronous, but the renderer would remember which modules got rendered, so that it can increase their priority as they're being sent to the client interleaved with data and HTML. |
This is almost certainly outside the scope of this RFC but I wanted to see if there was any interest in lazy imports as a codebase security mechanism. What I'm thinking of is a paradigm where the code is stored in the cloud (Firebase Storage or the like) and streamed to and loaded by the client contingent on some separate backend user authentication. |
Going with this limited version ( |
View formatted RFC
Note: I just wrote up the RFC. The semantics are designed by @sebmarkbage and @acdlite.