diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index 27b30f9bb03e2..f15cb5557e5c8 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -582,11 +582,6 @@ describe('ReactDOMServer', () => { ); ReactDOMServer.renderToString(); }).toThrow('ReactDOMServer does not yet support lazy-loaded components.'); - - expect(() => { - const FooPromise = {then() {}}; - ReactDOMServer.renderToString(); - }).toThrow('ReactDOMServer does not yet support lazy-loaded components.'); }); it('should throw (in dev) when children are mutated during render', () => { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 5d1ad4eb526ca..17b8932e6973c 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -444,15 +444,18 @@ describe('ReactDOMServerHydration', () => { }); it('should be able to use lazy components after hydrating', async () => { - const Lazy = new Promise(resolve => { - setTimeout( - () => - resolve(function World() { - return 'world'; - }), - 1000, - ); - }); + const Lazy = React.lazy( + () => + new Promise(resolve => { + setTimeout( + () => + resolve(function World() { + return 'world'; + }), + 1000, + ); + }), + ); class HelloWorld extends React.Component { state = {isClient: false}; componentDidMount() { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index dc79c3fb28031..7d6cfc729e291 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -38,6 +38,7 @@ import { REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, + REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; import { @@ -1005,14 +1006,11 @@ class ReactDOMServerRenderer { this.stack.push(frame); return ''; } - default: - if (typeof elementType.then === 'function') { - invariant( - false, - 'ReactDOMServer does not yet support lazy-loaded components.', - ); - } - break; + case REACT_LAZY_TYPE: + invariant( + false, + 'ReactDOMServer does not yet support lazy-loaded components.', + ); } } diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 3f508efe63f2d..3ad8fccdcddfa 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -60,6 +60,7 @@ import { REACT_CONCURRENT_MODE_TYPE, REACT_SUSPENSE_TYPE, REACT_PURE_TYPE, + REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -461,12 +462,9 @@ export function createFiberFromElement( case REACT_PURE_TYPE: fiberTag = PureComponent; break getTag; - default: { - if (typeof type.then === 'function') { - fiberTag = IndeterminateComponent; - break getTag; - } - } + case REACT_LAZY_TYPE: + fiberTag = IndeterminateComponent; + break getTag; } } let info = ''; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6dfed8599f8b9..0cf92b4b27342 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -104,12 +104,13 @@ import { updateClassInstance, } from './ReactFiberClassComponent'; import {readLazyComponentType} from './ReactFiberLazyComponent'; -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent'; import { resolveLazyComponentTag, createFiberFromFragment, createWorkInProgress, } from './ReactFiber'; +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -728,7 +729,7 @@ function mountIndeterminateComponent( if ( typeof Component === 'object' && Component !== null && - typeof Component.then === 'function' + Component.$$typeof === REACT_LAZY_TYPE ) { // We can't start a User Timing measurement with correct label yet. // Cancel and resume right after we know the tag. @@ -1422,7 +1423,7 @@ function beginWork( } case ClassComponentLazy: { const thenable = workInProgress.type; - const Component = getResultFromResolvedThenable(thenable); + const Component = getResultFromResolvedLazyComponent(thenable); if (isLegacyContextProvider(Component)) { pushLegacyContextProvider(workInProgress); } @@ -1498,7 +1499,7 @@ function beginWork( } case FunctionComponentLazy: { const thenable = workInProgress.type; - const Component = getResultFromResolvedThenable(thenable); + const Component = getResultFromResolvedLazyComponent(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updateFunctionComponent( current, @@ -1523,7 +1524,7 @@ function beginWork( } case ClassComponentLazy: { const thenable = workInProgress.type; - const Component = getResultFromResolvedThenable(thenable); + const Component = getResultFromResolvedLazyComponent(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updateClassComponent( current, @@ -1565,7 +1566,7 @@ function beginWork( } case ForwardRefLazy: { const thenable = workInProgress.type; - const Component = getResultFromResolvedThenable(thenable); + const Component = getResultFromResolvedLazyComponent(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updateForwardRef( current, @@ -1608,7 +1609,7 @@ function beginWork( } case PureComponentLazy: { const thenable = workInProgress.type; - const Component = getResultFromResolvedThenable(thenable); + const Component = getResultFromResolvedLazyComponent(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updatePureComponent( current, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e3e5df52683ae..d13bd2c66b8df 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -42,7 +42,7 @@ import { } from 'shared/ReactWorkTags'; import {Placement, Ref, Update} from 'shared/ReactSideEffectTags'; import invariant from 'shared/invariant'; -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent'; import { createInstance, @@ -552,7 +552,7 @@ function completeWork( break; } case ClassComponentLazy: { - const Component = getResultFromResolvedThenable(workInProgress.type); + const Component = getResultFromResolvedLazyComponent(workInProgress.type); if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 246aabda81b09..4ef224e2636f2 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -20,7 +20,7 @@ import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; import checkPropTypes from 'prop-types/checkPropTypes'; -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; @@ -298,7 +298,7 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { break; } case ClassComponentLazy: { - const Component = getResultFromResolvedThenable(node.type); + const Component = getResultFromResolvedLazyComponent(node.type); if (isContextProvider(Component)) { return node.stateNode.__reactInternalMemoizedMergedChildContext; } diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index 05d9d193c9268..f39773a6777bb 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -7,26 +7,28 @@ * @flow */ -import type {Thenable} from 'shared/ReactLazyComponent'; +import type {LazyComponent} from 'shared/ReactLazyComponent'; import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent'; -export function readLazyComponentType(thenable: Thenable): T { - const status = thenable._reactStatus; +export function readLazyComponentType(lazyComponent: LazyComponent): T { + const status = lazyComponent._status; switch (status) { case Resolved: - const Component: T = thenable._reactResult; + const Component: T = lazyComponent._result; return Component; case Rejected: - throw thenable._reactResult; + throw lazyComponent._result; case Pending: - throw thenable; + throw lazyComponent; default: { - thenable._reactStatus = Pending; + lazyComponent._status = Pending; + const ctor = lazyComponent._ctor; + const thenable = ctor(); thenable.then( resolvedValue => { - if (thenable._reactStatus === Pending) { - thenable._reactStatus = Resolved; + 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 @@ -39,13 +41,13 @@ export function readLazyComponentType(thenable: Thenable): T { } else { resolvedValue = resolvedValue; } - thenable._reactResult = resolvedValue; + lazyComponent._result = resolvedValue; } }, error => { - if (thenable._reactStatus === Pending) { - thenable._reactStatus = Rejected; - thenable._reactResult = error; + if (lazyComponent._status === Pending) { + lazyComponent._status = Rejected; + lazyComponent._result = error; } }, ); diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 4d383f3354983..75a7efb59b111 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -31,7 +31,7 @@ import { import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent'; import {getPublicInstance} from './ReactFiberHostConfig'; import { @@ -107,7 +107,7 @@ function getContextForSubtree( return processChildContext(fiber, Component, parentContext); } } else if (fiber.tag === ClassComponentLazy) { - const Component = getResultFromResolvedThenable(fiber.type); + const Component = getResultFromResolvedLazyComponent(fiber.type); if (isLegacyContextProvider(Component)) { return processChildContext(fiber, Component, parentContext); } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 413fd10213138..c3a0e983ebb55 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -53,7 +53,7 @@ import { import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent'; import { scheduleTimeout, @@ -312,7 +312,9 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { break; } case ClassComponentLazy: { - const Component = getResultFromResolvedThenable(failedUnitOfWork.type); + const Component = getResultFromResolvedLazyComponent( + failedUnitOfWork.type, + ); if (isLegacyContextProvider(Component)) { popLegacyContext(failedUnitOfWork); } diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js new file mode 100644 index 0000000000000..e39fc79c83884 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -0,0 +1,309 @@ +let React; +let ReactTestRenderer; +let ReactFeatureFlags; +let Suspense; +let lazy; + +describe('ReactLazy', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + React = require('react'); + Suspense = React.unstable_Suspense; + lazy = React.lazy; + ReactTestRenderer = require('react-test-renderer'); + }); + + function Text(props) { + ReactTestRenderer.unstable_yield(props.text); + return props.text; + } + + it('suspends until module has loaded', async () => { + const LazyText = lazy(async () => Text); + + const root = ReactTestRenderer.create( + }> + + , + { + unstable_isConcurrent: true, + }, + ); + + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + + 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'); + }); + + it('uses `default` property, if it exists', async () => { + const LazyText = lazy(async () => ({default: Text})); + + const root = ReactTestRenderer.create( + }> + + , + { + unstable_isConcurrent: true, + }, + ); + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + + 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'); + }); + + it('throws if promise rejects', async () => { + const LazyText = lazy(async () => { + throw new Error('Bad network'); + }); + + const root = ReactTestRenderer.create( + }> + + , + { + unstable_isConcurrent: true, + }, + ); + + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + + try { + await LazyText; + } catch (e) {} + + expect(root).toFlushAndThrow('Bad network'); + }); + + it('mount and reorder', async () => { + class Child extends React.Component { + componentDidMount() { + ReactTestRenderer.unstable_yield('Did mount: ' + this.props.label); + } + componentDidUpdate() { + ReactTestRenderer.unstable_yield('Did update: ' + this.props.label); + } + render() { + return ; + } + } + + const LazyChildA = lazy(async () => Child); + const LazyChildB = lazy(async () => Child); + + function Parent({swap}) { + return ( + }> + {swap + ? [ + , + , + ] + : [ + , + , + ]} + + ); + } + + const root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + + await LazyChildA; + await LazyChildB; + + expect(root).toFlushAndYield(['A', 'B', 'Did mount: A', 'Did mount: B']); + expect(root).toMatchRenderedOutput('AB'); + + // Swap the position of A and B + root.update(); + expect(root).toFlushAndYield(['B', 'A', 'Did update: B', 'Did update: A']); + expect(root).toMatchRenderedOutput('BA'); + }); + + it('resolves defaultProps, on mount and update', async () => { + function T(props) { + return ; + } + T.defaultProps = {text: 'Hi'}; + const LazyText = lazy(async () => T); + + const root = ReactTestRenderer.create( + }> + + , + { + unstable_isConcurrent: true, + }, + ); + + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + + await LazyText; + + expect(root).toFlushAndYield(['Hi']); + expect(root).toMatchRenderedOutput('Hi'); + + T.defaultProps = {text: 'Hi again'}; + root.update( + }> + + , + ); + expect(root).toFlushAndYield(['Hi again']); + expect(root).toMatchRenderedOutput('Hi again'); + }); + + it('resolves defaultProps without breaking memoization', async () => { + function LazyImpl(props) { + ReactTestRenderer.unstable_yield('Lazy'); + return ( + + + {props.children} + + ); + } + LazyImpl.defaultProps = {siblingText: 'Sibling'}; + const Lazy = lazy(async () => LazyImpl); + + class Stateful extends React.Component { + state = {text: 'A'}; + render() { + return ; + } + } + + const stateful = React.createRef(null); + + const root = ReactTestRenderer.create( + }> + + + + , + { + unstable_isConcurrent: true, + }, + ); + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + + await Lazy; + + expect(root).toFlushAndYield(['Lazy', 'Sibling', 'A']); + expect(root).toMatchRenderedOutput('SiblingA'); + + // Lazy should not re-render + stateful.current.setState({text: 'B'}); + expect(root).toFlushAndYield(['B']); + expect(root).toMatchRenderedOutput('SiblingB'); + }); + + it('includes lazy-loaded component in warning stack', async () => { + const LazyFoo = lazy(() => { + ReactTestRenderer.unstable_yield('Started loading'); + const Foo = props =>
{[, ]}
; + return Promise.resolve(Foo); + }); + + const root = ReactTestRenderer.create( + }> + + , + { + unstable_isConcurrent: true, + }, + ); + + expect(root).toFlushAndYield(['Started loading', 'Loading...']); + expect(root).toMatchRenderedOutput(null); + + await LazyFoo; + + expect(() => { + expect(root).toFlushAndYield(['A', 'B']); + }).toWarnDev(' in Text (at **)\n' + ' in Foo (at **)'); + expect(root).toMatchRenderedOutput(
AB
); + }); + + it('supports class and forwardRef components', async () => { + const LazyClass = lazy(async () => { + class Foo extends React.Component { + render() { + return ; + } + } + return Foo; + }); + + const LazyForwardRef = lazy(async () => { + class Bar extends React.Component { + render() { + return ; + } + } + return React.forwardRef((props, ref) => { + ReactTestRenderer.unstable_yield('forwardRef'); + return ; + }); + }); + + const ref = React.createRef(); + const root = ReactTestRenderer.create( + }> + + + , + { + unstable_isConcurrent: true, + }, + ); + + expect(root).toFlushAndYield(['Loading...']); + expect(root).toMatchRenderedOutput(null); + expect(ref.current).toBe(null); + + await LazyClass; + await LazyForwardRef; + + expect(root).toFlushAndYield(['Foo', 'forwardRef', 'Bar']); + expect(root).toMatchRenderedOutput('FooBar'); + expect(ref.current).not.toBe(null); + }); +}); diff --git a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js index e9744254bbd8c..e26259320c446 100644 --- a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js @@ -42,9 +42,12 @@ describe('pure', () => { function Indirection(props) { return ; } - return Promise.resolve(Indirection); + return React.lazy(async () => Indirection); + }); + sharedTests('lazy', (...args) => { + const Pure = React.pure(...args); + return React.lazy(async () => Pure); }); - sharedTests('lazy', (...args) => Promise.resolve(React.pure(...args))); function sharedTests(label, pure) { describe(`${label}`, () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 3fbd5e4e77254..5071e52b4b7d4 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -6,7 +6,6 @@ let ReactCache; let Suspense; let StrictMode; let ConcurrentMode; -let lazy; let cache; let TextResource; @@ -28,7 +27,6 @@ describe('ReactSuspenseWithNoopRenderer', () => { Suspense = React.unstable_Suspense; StrictMode = React.StrictMode; ConcurrentMode = React.unstable_ConcurrentMode; - lazy = React.lazy; function invalidateCache() { cache = ReactCache.createCache(invalidateCache); @@ -50,12 +48,12 @@ describe('ReactSuspenseWithNoopRenderer', () => { textResourceShouldFail = false; }); - function div(...children) { - children = children.map( - c => (typeof c === 'string' ? {text: c, hidden: false} : c), - ); - return {type: 'div', children, prop: undefined, hidden: false}; - } + // function div(...children) { + // children = children.map( + // c => (typeof c === 'string' ? {text: c, hidden: false} : c), + // ); + // return {type: 'div', children, prop: undefined, hidden: false}; + // } function span(prop) { return {type: 'span', children: [], prop, hidden: false}; @@ -1416,294 +1414,6 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); }); - describe('Promise as element type', () => { - it('accepts a promise as an element type', async () => { - const LazyText = Promise.resolve(Text); - - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await LazyText; - - expect(ReactNoop.flush()).toEqual(['Hi']); - expect(ReactNoop.getChildren()).toEqual([span('Hi')]); - - // Should not suspend on update - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Hi again']); - expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); - }); - - it('throws if promise rejects', async () => { - const LazyText = Promise.reject(new Error('Bad network')); - - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Loading...']); - - await LazyText.catch(() => {}); - - expect(() => ReactNoop.flush()).toThrow('Bad network'); - }); - - it('mount and reorder', async () => { - class Child extends React.Component { - componentDidMount() { - ReactNoop.yield('Did mount: ' + this.props.label); - } - componentDidUpdate() { - ReactNoop.yield('Did update: ' + this.props.label); - } - render() { - return ; - } - } - - const LazyChildA = Promise.resolve(Child); - const LazyChildB = Promise.resolve(Child); - - function Parent({swap}) { - return ( - }> - {swap - ? [ - , - , - ] - : [ - , - , - ]} - - ); - } - - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await LazyChildA; - await LazyChildB; - - expect(ReactNoop.flush()).toEqual([ - 'A', - 'B', - 'Did mount: A', - 'Did mount: B', - ]); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); - - // Swap the position of A and B - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'B', - 'A', - 'Did update: B', - 'Did update: A', - ]); - expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); - }); - - it('uses `default` property, if it exists', async () => { - const LazyText = Promise.resolve({default: Text}); - - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await LazyText; - - expect(ReactNoop.flush()).toEqual(['Hi']); - expect(ReactNoop.getChildren()).toEqual([span('Hi')]); - - // Should not suspend on update - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Hi again']); - expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); - }); - - it('resolves defaultProps, on mount and update', async () => { - function T(props) { - return ; - } - T.defaultProps = {text: 'Hi'}; - const LazyText = Promise.resolve(T); - - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await LazyText; - - expect(ReactNoop.flush()).toEqual(['Hi']); - expect(ReactNoop.getChildren()).toEqual([span('Hi')]); - - T.defaultProps = {text: 'Hi again'}; - - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Hi again']); - expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); - }); - - it('resolves defaultProps without breaking memoization', async () => { - function LazyImpl(props) { - ReactNoop.yield('Lazy'); - return ( - - - {props.children} - - ); - } - LazyImpl.defaultProps = {siblingText: 'Sibling'}; - const Lazy = Promise.resolve(LazyImpl); - - class Stateful extends React.Component { - state = {text: 'A'}; - render() { - return ; - } - } - - const stateful = React.createRef(null); - ReactNoop.render( - }> - - - - , - ); - expect(ReactNoop.flush()).toEqual(['Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - await Lazy; - expect(ReactNoop.flush()).toEqual(['Lazy', 'Sibling', 'A']); - expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('A')]); - - // Lazy should not re-render - stateful.current.setState({text: 'B'}); - expect(ReactNoop.flush()).toEqual(['B']); - expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('B')]); - }); - - it('lazy-load using React.lazy', async () => { - const LazyText = lazy(() => { - ReactNoop.yield('Started loading'); - return Promise.resolve(Text); - }); - - ReactNoop.render( - }> - - - - , - ); - // Render first two siblings. The lazy component should not have - // started loading yet. - ReactNoop.flushThrough(['A', 'B']); - - // Flush the rest. - expect(ReactNoop.flush()).toEqual(['Started loading', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await LazyText; - - expect(ReactNoop.flush()).toEqual(['A', 'B', 'C']); - expect(ReactNoop.getChildren()).toEqual([ - span('A'), - span('B'), - span('C'), - ]); - }); - - it('includes lazy-loaded component in warning stack', async () => { - const LazyFoo = lazy(() => { - ReactNoop.yield('Started loading'); - const Foo = props => ( -
{[, ]}
- ); - return Promise.resolve(Foo); - }); - - ReactNoop.render( - }> - - , - ); - expect(ReactNoop.flush()).toEqual(['Started loading', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await LazyFoo; - expect(() => { - expect(ReactNoop.flush()).toEqual(['A', 'B']); - }).toWarnDev(' in Text (at **)\n' + ' in Foo (at **)'); - expect(ReactNoop.getChildren()).toEqual([div(span('A'), span('B'))]); - }); - - it('supports class and forwardRef components', async () => { - const LazyClass = lazy(() => { - class Foo extends React.Component { - render() { - return ; - } - } - return Promise.resolve(Foo); - }); - - const LazyForwardRef = lazy(() => { - const Bar = React.forwardRef((props, ref) => ( - - )); - return Promise.resolve(Bar); - }); - - const ref = React.createRef(); - ReactNoop.render( - }> - - - , - ); - expect(ReactNoop.flush()).toEqual(['Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - expect(ref.current).toBe(null); - - await LazyClass; - await LazyForwardRef; - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']); - expect(ReactNoop.getChildren()).toEqual([span('Foo'), span('Bar')]); - expect(ref.current).not.toBe(null); - }); - }); - it('does not call lifecycles of a suspended component', async () => { class TextWithLifecycle extends React.Component { componentDidMount() { diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 85fa687ec3d6a..28415fcf265c4 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -5,23 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -type Thenable = { - then(resolve: (T) => mixed, reject: (mixed) => mixed): R, -}; +import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent'; -export function lazy(ctor: () => Thenable) { - let thenable = null; +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; + +export function lazy(ctor: () => Thenable): LazyComponent { return { - then(resolve, reject) { - if (thenable === null) { - // Lazily create thenable by wrapping in an extra thenable. - thenable = ctor(); - ctor = null; - } - return thenable.then(resolve, reject); - }, + $$typeof: REACT_LAZY_TYPE, + _ctor: ctor, // React uses these fields to store the result. - _reactStatus: -1, - _reactResult: null, + _status: -1, + _result: null, }; } diff --git a/packages/shared/ReactLazyComponent.js b/packages/shared/ReactLazyComponent.js index 0d980f71834a3..b5a5df9369525 100644 --- a/packages/shared/ReactLazyComponent.js +++ b/packages/shared/ReactLazyComponent.js @@ -7,30 +7,36 @@ * @flow */ -export type Thenable = { - then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, - _reactStatus?: 0 | 1 | 2, - _reactResult: any, +export type Thenable = { + then(resolve: (T) => mixed, reject: (mixed) => mixed): R, }; -type ResolvedThenable = { - then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, - _reactStatus?: 1, - _reactResult: T, +export type LazyComponent = { + $$typeof: Symbol | number, + _ctor: () => Thenable, + _status: 0 | 1 | 2, + _result: any, +}; + +type ResolvedLazyComponentThenable = { + $$typeof: Symbol | number, + _ctor: () => Thenable, + _status: 1, + _result: any, }; export const Pending = 0; export const Resolved = 1; export const Rejected = 2; -export function getResultFromResolvedThenable( - thenable: ResolvedThenable, +export function getResultFromResolvedLazyComponent( + lazyComponent: ResolvedLazyComponentThenable, ): T { - return thenable._reactResult; + return lazyComponent._result; } -export function refineResolvedThenable( - thenable: Thenable, -): ResolvedThenable | null { - return thenable._reactStatus === Resolved ? thenable._reactResult : null; +export function refineResolvedLazyComponent( + lazyComponent: LazyComponent, +): ResolvedLazyComponentThenable | null { + return lazyComponent._status === Resolved ? lazyComponent._result : null; } diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 774c1bbe20385..2b8211474c384 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -42,6 +42,7 @@ export const REACT_SUSPENSE_TYPE = hasSymbol ? Symbol.for('react.suspense') : 0xead1; export const REACT_PURE_TYPE = hasSymbol ? Symbol.for('react.pure') : 0xead3; +export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 4e89efef4acfa..abc5b04916bca 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactLazyComponent'; +import type {LazyComponent} from 'shared/ReactLazyComponent'; import warningWithoutStack from 'shared/warningWithoutStack'; import { @@ -21,8 +21,9 @@ import { REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, + REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; -import {refineResolvedThenable} from 'shared/ReactLazyComponent'; +import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; function getWrappedName( outerType: mixed, @@ -80,12 +81,12 @@ function getComponentName(type: mixed): string | null { return getWrappedName(type, type.render, 'ForwardRef'); case REACT_PURE_TYPE: return getWrappedName(type, type.render, 'Pure'); - } - if (typeof type.then === 'function') { - const thenable: Thenable = (type: any); - const resolvedThenable = refineResolvedThenable(thenable); - if (resolvedThenable) { - return getComponentName(resolvedThenable); + case REACT_LAZY_TYPE: { + const thenable: LazyComponent = (type: any); + const resolvedThenable = refineResolvedLazyComponent(thenable); + if (resolvedThenable) { + return getComponentName(resolvedThenable); + } } } } diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index 8682a3fb53932..f662ce19fa699 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -17,6 +17,7 @@ import { REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, REACT_PURE_TYPE, + REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -31,7 +32,7 @@ export default function isValidElementType(type: mixed) { type === REACT_SUSPENSE_TYPE || (typeof type === 'object' && type !== null && - (typeof type.then === 'function' || + (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_PURE_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE ||