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
Add support for named exports #483
Conversation
Will this feature be in next release ? |
|
455d33d
to
7680fd9
Compare
I've added documentation to the website to go with each commit
I thought of "watchdog" because its like a parallel process that supervises the loading process and may intervene if it goes wrong. "guard" also sounds fine to me, so I've updated it to be called "guard". That probably fits better with delays (the guard took a while to let the promise through) vs timeouts (the guard gave up waiting for the promise and rendered the error state). |
Also, I've put the commit that changes the Babel plugin last. So if you wanted to delay the breaking change, this could be released in two steps:
|
7680fd9
to
70183d0
Compare
packages/babel-plugin/src/index.js
Outdated
if (callPaths.length === 0) return | ||
if (!isFunctionAndOnlyReturnsImport(importCreator)) { | ||
throw new Error( | ||
'The first argument to `loadable()` must be a function with a single statement that returns a call to `import()`' |
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 would "fix" some user code. By breaking the compilation.
The error should point somewhere to loadable
readme with a brief explanation of a problem and a way to resolve it - resolveComponent
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 added a link in the error message to the notes for loadFn
, which in turn link to the docs for guard, delay/timeout, and resolveComponent
@@ -33,6 +34,23 @@ function createLoadable({ resolve = identity, render, onLoad }) { | |||
return null | |||
} | |||
|
|||
function resolve(module, props, Loadable) { | |||
const Component = options.resolveComponent |
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.
according to this code, resolveComponent
should not return yet another promise. And I reckon someone would try to.
Could we check that Component
is ReactIs.isValidElementType
?
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.
Done. That prevents you from using resolveComponent
with loadable.lib
, but then there is no point doing that (since you get the entire module anyway).
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.
Also I had to bump up the maximum bundle size
You have got the approval from me. I like the change to the API, and I like the implementation. @gregberge - please take a look. |
2418598
to
7f0a4c7
Compare
Hello guys! @hedgepigdaniel @theKashey First, thanks for your work on this feature! It is great! I think we could make the API more generic and more powerful. I explain my idea: import loadable from '@loadable/component'
const Loadable = loadable(() => import('./components'))
// this function will only be called on client-side, it can be asynchronous
Loadable.getContext = async ({ props }) => {
// we actually load data dynamically with the component
const book = await getBook(props.id)
// we could also add a default delay
await delay(200)
// we are able to change the component
return { books: { [props.id]: book }
}
// this function has to be synchronous, it is called on client and server
Loadable.resolve = ({ $component, $module, props, ctx }) => {
// we got some data from context
const data = ctx.books[props.id]
// we are able to change the component
return { $component: $module.Foo, props: { ...props, data } }
}
<Loadable id="10" /> On the server, we will be able to pass a context: const book = await getBook(req.params.id)
const ctx = { books: { [req.params.id]: book } }
const extractor = new ChunkExtractor({ statsFile, ctx }) If Suspense is available one day, resolveContext could be called on server-side. For now it is not possible. What do you think? |
Oups I closed it! |
|
`resolveComponent` is a synchronous function to resolve the react component from the imported module and props. Unlike wrappers on the import function itself, it works on both the client and server side.
7f0a4c7
to
e610e02
Compare
hey, thanks for taking a look at this.
Having a way of passing extra props to the component seems neat, but I'm not sure how to do it consistently without suspense. If you have to set up 2 different methods for SSR and client side, I suspect using an external state management instead (e.g. Redux + route based data loading) would work better. |
You are right. Data fetching is another problem, not our business here. Your implementation is nice. I am wondering if guard is enough explicit or not. |
Well, let's think what it is:
It could be a Another question - why we might use it:
Read: there is a better way, there is a better way, it's better to use a So what about just not mark this option as an experimental? I am pretty sure not many people were using this functionality so far, as long as they would had their SSR broken, and |
Another option is to have options more specifically tailored to common use cases. For example:
Marking it as experimental or simply not adding the option is also fine (and therefore not making the babel plugin complain basically the first 2 commits only). That still makes named exports possible, which is the most important thing. My thinking in including that change here is that it is confusing that the current |
Which would not be called if this components was SSRed and loaded in a "sync" way, so please don't put any valuable logic inside. 😒 From a customer point of view it would be great to have just a global option to control |
e610e02
to
a47d3d9
Compare
https://webpack.js.org/configuration/output/#outputchunkloadtimeout
Naming imports are not a good idea because you don't have Tree Shaking. We have to warn about it on the documentation. As soon as I have time, I will merge this one and work on it. Last week-end I was working on SVGR. One project per week-end :). |
That's not quite bound to this case. It's more about - you will not get treeshaking if you have more that one thingy exported from a module, and more that one thingy consumed. |
Yeah, the point is maybe we just have to "not support named exports". Creating a module wrapper is not a big deal and it is a good practice for tree shaking. @hedgepigdaniel I am interested to have your advice on this. |
Ok, so it sounds like we are trying to work out what is the use case for named exports. Here's a few use cases (using a module wrapper with a default export breaks these use cases): 1. Project where named exports are used for DX reasonsDefault exports encourage the same thing to have different names in different files. This is confusing, prevents easy refactoring, prevents good support by tooling (e.g. import auto complete), and adds to cognitive load. See this short and sweet post for more info. 2. Connecting multiple loadable components to the same split pointTake an example that there is a modal that the user may open, and the modal that has tabs inside it. Users usually explore all the tabs after they open the modal. There are 5 tabs. 2 tabs are visible to all users, but 3 tabs are only accessible to administrators.. The content of the tabs is reasonably small (not much data to download), but each request still adds latency. In this case, it might be a good optimization to use the same split point for all of the 3 components used in the administrator tabs. That way if a user opens one of the tabs, the code for all the tabs is downloaded, so there is no extra latency when they switch to other tabs. It's not possible for webpack to determine which split points will be loaded at the same time at runtime, so allowing multiple named exports in a single points allows the user to give webpack that information so that it can make a more optimal choice of which code to put in which chunks. There seems to be a few users interested in this ability: #245 (comment) To put it really simply, javascript supports named exports, webpack supports named exports, so loadable-components should support named exports. The fact that it currently doesn't is surprising and inconsistent with the rest of the ecosystem. Re tree shaking, it seems like its a fundamental limitation with the current javascript that |
Ok. So in short - the main advantage for named exports is the ability to use them "more often" without overthinking the problem and extra boilerplate. |
@hedgepigdaniel the chunk name resolves the second issue. Webpack groups chunks with the same name. Also the case you mentioned is not the nominal one, I think most of time users will use it for wrong reasons and just lose the tree shaking. |
No, chunk names does not resolve the issue. Chunk names lets you force webpack to put things in the same chunk, but:
The idiomatic way to have multiple things behind a single split point is to export multiple things from one module, with named exports. No non standard features, no hacks to force a specific compiler to do what you want, just using the standard features of the language. |
You only "lose" tree shaking to the extent that you put multiple named exports in one file anyway - but loadable-components is currently broken even with a single named export. The use cases are already explained in the original PR description and the linked issue. This is clearly causing pain and confusion (and bugs in production), and there is a simple fix right here. @gregberge It's been almost 4 months now since this PR was opened, and you've already said twice in this thread that you will merge it as it is, but every 2 weeks you change your mind. How much longer do we have to continue this discussion? |
@hedgepigdaniel since I am not very involved in the project it takes time and I am sorry for that. Yeah it is broken but we have two choices:
https://reactjs.org/docs/code-splitting.html#named-exports "React.lazy currently only supports default exports." The currently makes me think that it is not definitive. Let's make it possible. Also @hedgepigdaniel are you interested to be more involved in the project? Maintainer? |
This project? I thought you were the author of it? Sure, I'm happy to help with maintaining. HMU if you want to chat about it. |
Morning @gregberge @hedgepigdaniel thanks for your work on this - reading through the thread it seems like this has been ready to ship for a while. From a consumer perspective, it'd be great to see that happen as it's probably a blocker for us considering we'd have to refactor our project to use default exports. |
Meanwhile...
So - if you are landing to a subroute, you have to "see" how parent page is loading. The right fix for the problemRefactor all routing, so the problem will not exists. The "faster" fix for the problemHoist
👍 make it possible. |
Would love to see this merged soon! I really want to use the package in my org's codebase but we have a ton of named exports |
Hi @hedgepigdaniel and @gregberge what is the plan for this? |
I also would love to see it merged in the main tree. |
Then let's do it! |
+1, would love to see this merged, happy to help fix any blockers |
+1, looking forward to seeing this merged as soon as possible :) |
const Component = options.resolveComponent | ||
? options.resolveComponent(module, props) | ||
: defaultResolveComponent(module) | ||
if (options.resolveComponent && !ReactIs.isValidElementType(Component)) { |
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.
- to double check how this plays with
loadable.lib
Ok. So the only missing piece is to update |
where are the docs for this? I don't see it on the site anywhere? |
Ah yes thanks! |
Add support for named exports
Summary
See #245
Currently to create a loadable component, the module that is asynchronously loaded must export the component as the default export (or be a commonjs module exporting the component as
module.exports
). This is problematic/limiting because:Detailed choices
See #245 for preliminary discussion of the API
loadable
andloadable.lib
still accept the import creator as the first argument. This means that for the common/simple case ofloadable(() => import('./xyz'))
, there is no breaking change, instead options are added.resolveComponent
is optional and defaults to the existing behaviour. Again, no breaking change, but typescript type inference (for the props of the loadable component) does not work unless it is specified. I think this is a consequence of the CommonJS compatibility.Benefits
Ability to use named exports (including on the server side)
Use
resolveComponent
to select the exportAbility to create multiple loadable components from a single split point
resloveComponent
has access to propsTypescript type inference
When using
resolveComponent
, typescript is able to infer the type of the Props of the loadable component (previously they were alwaysunknown
). I think the default type/implementation ofresolveComponent
breaks type inference somehow by supporting commonjs default exports.Before
After
Test plan
Existing tests have been changed where necessary and new tests have been added for new functionality