diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index f15cb5557e5c8..88b47fde827ee 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -571,14 +571,19 @@ describe('ReactDOMServer', () => { ReactDOMServer.renderToString(); }).toThrow('ReactDOMServer does not yet support Suspense.'); + async function fakeImport(result) { + return {default: result}; + } + expect(() => { - const LazyFoo = React.lazy( - () => + const LazyFoo = React.lazy(() => + fakeImport( new Promise(resolve => resolve(function Foo() { return
; }), ), + ), ); ReactDOMServer.renderToString(); }).toThrow('ReactDOMServer does not yet support lazy-loaded components.'); diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 17b8932e6973c..3bd57a8e7eec1 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -444,14 +444,20 @@ describe('ReactDOMServerHydration', () => { }); it('should be able to use lazy components after hydrating', async () => { + async function fakeImport(result) { + return {default: result}; + } + const Lazy = React.lazy( () => new Promise(resolve => { setTimeout( () => - resolve(function World() { - return 'world'; - }), + resolve( + fakeImport(function World() { + return 'world'; + }), + ), 1000, ); }), diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index f39773a6777bb..be7c39d66823d 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -10,6 +10,7 @@ import type {LazyComponent} from 'shared/ReactLazyComponent'; import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent'; +import warning from 'shared/warning'; export function readLazyComponentType(lazyComponent: LazyComponent): T { const status = lazyComponent._status; @@ -26,22 +27,22 @@ export function readLazyComponentType(lazyComponent: LazyComponent): T { const ctor = lazyComponent._ctor; const thenable = ctor(); thenable.then( - resolvedValue => { + moduleObject => { if (lazyComponent._status === Pending) { - lazyComponent._status = Resolved; - if (typeof resolvedValue === 'object' && resolvedValue !== null) { - // If the `default` property is not empty, assume it's the result - // of an async import() and use that. Otherwise, use the - // resolved value itself. - const defaultExport = (resolvedValue: any).default; - resolvedValue = - defaultExport !== undefined && defaultExport !== null - ? defaultExport - : resolvedValue; - } else { - resolvedValue = resolvedValue; + const defaultExport = moduleObject.default; + if (__DEV__) { + if (defaultExport === undefined) { + warning( + false, + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: %s\n\nYour code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))", + moduleObject, + ); + } } - lazyComponent._result = resolvedValue; + lazyComponent._status = Resolved; + lazyComponent._result = defaultExport; } }, error => { diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index a6bb1a7663b6e..7faa68ce85ffd 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -573,6 +573,10 @@ describe('ReactDebugFiberPerf', () => { return ; } + async function fakeImport(result) { + return {default: result}; + } + let resolve; const LazyFoo = React.lazy( () => @@ -591,9 +595,11 @@ describe('ReactDebugFiberPerf', () => { ReactNoop.flush(); expect(getFlameChart()).toMatchSnapshot(); - resolve(function Foo() { - return
; - }); + resolve( + fakeImport(function Foo() { + return
; + }), + ); await LazyFoo; ReactNoop.render( diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index e39fc79c83884..4df941acd42a1 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -21,8 +21,12 @@ describe('ReactLazy', () => { return props.text; } + async function fakeImport(result) { + return {default: result}; + } + it('suspends until module has loaded', async () => { - const LazyText = lazy(async () => Text); + const LazyText = lazy(() => fakeImport(Text)); const root = ReactTestRenderer.create( }> @@ -51,8 +55,10 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('Hi again'); }); - it('uses `default` property, if it exists', async () => { - const LazyText = lazy(async () => ({default: Text})); + it('does not support arbitrary promises, only module objects', async () => { + spyOnDev(console, 'error'); + + const LazyText = lazy(async () => Text); const root = ReactTestRenderer.create( }> @@ -67,17 +73,13 @@ describe('ReactLazy', () => { await LazyText; - expect(root).toFlushAndYield(['Hi']); - expect(root).toMatchRenderedOutput('Hi'); - - // Should not suspend on update - root.update( - }> - - , - ); - expect(root).toFlushAndYield(['Hi again']); - expect(root).toMatchRenderedOutput('Hi again'); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Expected the result of a dynamic import() call', + ); + } + expect(root).toFlushAndThrow('Element type is invalid'); }); it('throws if promise rejects', async () => { @@ -117,8 +119,8 @@ describe('ReactLazy', () => { } } - const LazyChildA = lazy(async () => Child); - const LazyChildB = lazy(async () => Child); + const LazyChildA = lazy(() => fakeImport(Child)); + const LazyChildB = lazy(() => fakeImport(Child)); function Parent({swap}) { return ( @@ -160,7 +162,7 @@ describe('ReactLazy', () => { return ; } T.defaultProps = {text: 'Hi'}; - const LazyText = lazy(async () => T); + const LazyText = lazy(() => fakeImport(T)); const root = ReactTestRenderer.create( }> @@ -200,7 +202,7 @@ describe('ReactLazy', () => { ); } LazyImpl.defaultProps = {siblingText: 'Sibling'}; - const Lazy = lazy(async () => LazyImpl); + const Lazy = lazy(() => fakeImport(LazyImpl)); class Stateful extends React.Component { state = {text: 'A'}; @@ -239,7 +241,7 @@ describe('ReactLazy', () => { const LazyFoo = lazy(() => { ReactTestRenderer.unstable_yield('Started loading'); const Foo = props =>
{[, ]}
; - return Promise.resolve(Foo); + return fakeImport(Foo); }); const root = ReactTestRenderer.create( @@ -263,13 +265,13 @@ describe('ReactLazy', () => { }); it('supports class and forwardRef components', async () => { - const LazyClass = lazy(async () => { + const LazyClass = lazy(() => { class Foo extends React.Component { render() { return ; } } - return Foo; + return fakeImport(Foo); }); const LazyForwardRef = lazy(async () => { @@ -278,10 +280,12 @@ describe('ReactLazy', () => { return ; } } - return React.forwardRef((props, ref) => { - ReactTestRenderer.unstable_yield('forwardRef'); - return ; - }); + return fakeImport( + React.forwardRef((props, ref) => { + ReactTestRenderer.unstable_yield('forwardRef'); + return ; + }), + ); }); const ref = React.createRef(); diff --git a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js index e26259320c446..a800483b42c13 100644 --- a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js @@ -34,6 +34,10 @@ describe('pure', () => { return ; } + async function fakeImport(result) { + return {default: result}; + } + // Tests should run against both the lazy and non-lazy versions of `pure`. // To make the tests work for both versions, we wrap the non-lazy version in // a lazy function component. @@ -42,11 +46,11 @@ describe('pure', () => { function Indirection(props) { return ; } - return React.lazy(async () => Indirection); + return React.lazy(() => fakeImport(Indirection)); }); sharedTests('lazy', (...args) => { const Pure = React.pure(...args); - return React.lazy(async () => Pure); + return React.lazy(() => fakeImport(Pure)); }); function sharedTests(label, pure) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 5ce531841266f..0a6f6a445a461 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -256,7 +256,11 @@ describe('ReactSuspense', () => { } } - const LazyClass = React.lazy(() => Promise.resolve(Class)); + async function fakeImport(result) { + return {default: result}; + } + + const LazyClass = React.lazy(() => fakeImport(Class)); const root = ReactTestRenderer.create( }> diff --git a/packages/shared/ReactLazyComponent.js b/packages/shared/ReactLazyComponent.js index b5a5df9369525..b44620172a723 100644 --- a/packages/shared/ReactLazyComponent.js +++ b/packages/shared/ReactLazyComponent.js @@ -13,14 +13,14 @@ export type Thenable = { export type LazyComponent = { $$typeof: Symbol | number, - _ctor: () => Thenable, + _ctor: () => Thenable<{default: T}, mixed>, _status: 0 | 1 | 2, _result: any, }; -type ResolvedLazyComponentThenable = { +type ResolvedLazyComponent = { $$typeof: Symbol | number, - _ctor: () => Thenable, + _ctor: () => Thenable<{default: T}, mixed>, _status: 1, _result: any, }; @@ -30,13 +30,13 @@ export const Resolved = 1; export const Rejected = 2; export function getResultFromResolvedLazyComponent( - lazyComponent: ResolvedLazyComponentThenable, + lazyComponent: ResolvedLazyComponent, ): T { return lazyComponent._result; } export function refineResolvedLazyComponent( lazyComponent: LazyComponent, -): ResolvedLazyComponentThenable | null { +): ResolvedLazyComponent | null { return lazyComponent._status === Resolved ? lazyComponent._result : null; }