Skip to content

Commit

Permalink
Fix Issue with Undefined Lazy Imports By Refactoring Lazy Initializat…
Browse files Browse the repository at this point in the history
…ion Order (#21642)

* Add a DEV warning for common case

* Don't set Pending flag before we know it's a promise

* Move default exports extraction to render phase

This is really where most unwrapping happen. The resolved promise is the
module object and then we read things from it.

This way it lines up a bit closer with the Promise model too since the
promise resolving to React gets passed this same value.

If this throws, then it throws during render so it's caught properly and
you can break on it and even see it on the right stack.

* Check if the default is in the module object instead of if it's undefined

Normally we'd just check if something is undefined but in this case it's
valid to have an undefined value in the export but if you don't have a
property then you're probably importing the wrong kind of object.

* We need to check if it's uninitialized for sync resolution

Co-authored-by: Dan Abramov <[email protected]>
  • Loading branch information
sebmarkbage and gaearon authored Jun 7, 2021
1 parent 0eea577 commit 5aa0c56
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@ describe('ReactLazy', () => {

await Promise.resolve();

expect(Scheduler).toFlushAndThrow('Element type is invalid');
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledTimes(3);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Expected the result of a dynamic import() call',
);
}
expect(Scheduler).toFlushAndThrow('Element type is invalid');
});

it('throws if promise rejects', async () => {
Expand Down
63 changes: 42 additions & 21 deletions packages/react/src/ReactLazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type PendingPayload = {

type ResolvedPayload<T> = {
_status: 1,
_result: T,
_result: {default: T},
};

type RejectedPayload = {
Expand All @@ -53,43 +53,64 @@ function lazyInitializer<T>(payload: Payload<T>): T {
const ctor = payload._result;
const thenable = ctor();
// Transition to the next state.
const pending: PendingPayload = (payload: any);
pending._status = Pending;
pending._result = thenable;
// This might throw either because it's missing or throws. If so, we treat it
// as still uninitialized and try again next time. Which is the same as what
// happens if the ctor or any wrappers processing the ctor throws. This might
// end up fixing it if the resolution was a concurrency bug.
thenable.then(
moduleObject => {
if (payload._status === Pending) {
const defaultExport = moduleObject.default;
if (__DEV__) {
if (defaultExport === undefined) {
console.error(
'lazy: Expected the result of a dynamic import() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
// Break up imports to avoid accidentally parsing them as dependencies.
'const MyComponent = lazy(() => imp' +
"ort('./MyComponent'))",
moduleObject,
);
}
}
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const resolved: ResolvedPayload<T> = (payload: any);
resolved._status = Resolved;
resolved._result = defaultExport;
resolved._result = moduleObject;
}
},
error => {
if (payload._status === Pending) {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const rejected: RejectedPayload = (payload: any);
rejected._status = Rejected;
rejected._result = error;
}
},
);
if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
const pending: PendingPayload = (payload: any);
pending._status = Pending;
pending._result = thenable;
}
}
if (payload._status === Resolved) {
return payload._result;
const moduleObject = payload._result;
if (__DEV__) {
if (moduleObject === undefined) {
console.error(
'lazy: Expected the result of a dynamic import() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
// Break up imports to avoid accidentally parsing them as dependencies.
'const MyComponent = lazy(() => imp' +
"ort('./MyComponent'))\n\n" +
'Did you accidentally put curly braces around the import?',
moduleObject,
);
}
}
if (__DEV__) {
if (!('default' in moduleObject)) {
console.error(
'lazy: Expected the result of a dynamic import() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
// Break up imports to avoid accidentally parsing them as dependencies.
'const MyComponent = lazy(() => imp' +
"ort('./MyComponent'))",
moduleObject,
);
}
}
return moduleObject.default;
} else {
throw payload._result;
}
Expand Down

0 comments on commit 5aa0c56

Please sign in to comment.