From 270fb636b337ead13b03e5b4830154a405e9bb97 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 18 Oct 2018 17:22:17 -0700 Subject: [PATCH] Lazy components must use React.lazy Removes support for using arbitrary promises as the type of a React element. Instead, promises must be wrapped in React.lazy. This gives us flexibility later if we need to change the protocol. The reason is that promises do not provide a way to call their constructor multiple times. For example: const promiseForA = new Promise(resolve => { fetchA(a => resolve(a)); }); Given a reference to `promiseForA`, there's no way to call `fetchA` again. Calling `then` on the promise doesn't run the constructor again; it only attaches another listener. In the future we will likely introduce an API like `React.eager` that is similar to `lazy` but eagerly calls the constructor. That gives us the ability to call the constructor multiple times. E.g. to increase the priority, or to retry if the first operation failed. --- .../__tests__/ReactServerRendering-test.js | 5 - .../ReactServerRenderingHydration-test.js | 21 +- packages/react-reconciler/src/ReactFiber.js | 10 +- .../src/ReactFiberBeginWork.js | 3 +- .../src/ReactFiberLazyComponent.js | 26 +- .../src/__tests__/ReactLazy-test.internal.js | 309 ++++++++++++++++++ .../src/__tests__/ReactPure-test.internal.js | 7 +- ...tSuspenseWithNoopRenderer-test.internal.js | 302 +---------------- packages/react/src/ReactLazy.js | 13 +- packages/shared/ReactLazyComponent.js | 24 +- packages/shared/ReactSymbols.js | 1 + packages/shared/getComponentName.js | 15 +- packages/shared/isValidElementType.js | 3 +- 13 files changed, 386 insertions(+), 353 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js 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-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 36fe0dc1dec7d..08687d4f941fd 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -110,6 +110,7 @@ import { createFiberFromFragment, createWorkInProgress, } from './ReactFiber'; +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -729,7 +730,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. diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index 05d9d193c9268..8595e0e2a3cfd 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 {LazyComponentThenable} from 'shared/ReactLazyComponent'; import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent'; -export function readLazyComponentType(thenable: Thenable): T { - const status = thenable._reactStatus; +export function readLazyComponentType( + thenable: LazyComponentThenable, +): T { + const status = thenable._status; switch (status) { case Resolved: - const Component: T = thenable._reactResult; + const Component: T = thenable._result; return Component; case Rejected: - throw thenable._reactResult; + throw thenable._result; case Pending: throw thenable; default: { - thenable._reactStatus = Pending; + thenable._status = Pending; thenable.then( resolvedValue => { - if (thenable._reactStatus === Pending) { - thenable._reactStatus = Resolved; + if (thenable._status === Pending) { + thenable._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; + thenable._result = resolvedValue; } }, error => { - if (thenable._reactStatus === Pending) { - thenable._reactStatus = Rejected; - thenable._reactResult = error; + if (thenable._status === Pending) { + thenable._status = Rejected; + thenable._result = error; } }, ); 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 ca1bb36851641..d1a9f5542b92c 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, ref) { return ; } - return Promise.resolve(React.forwardRef(Indirection)); + return React.lazy(async () => React.forwardRef(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..4964f0ca7328b 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -5,13 +5,20 @@ * LICENSE file in the root directory of this source tree. */ +import type {LazyComponentThenable} from 'shared/ReactLazyComponent'; + type Thenable = { then(resolve: (T) => mixed, reject: (mixed) => mixed): R, }; -export function lazy(ctor: () => Thenable) { +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; + +export function lazy( + ctor: () => Thenable, +): LazyComponentThenable { let thenable = null; return { + $$typeof: REACT_LAZY_TYPE, then(resolve, reject) { if (thenable === null) { // Lazily create thenable by wrapping in an extra thenable. @@ -21,7 +28,7 @@ export function lazy(ctor: () => Thenable) { return thenable.then(resolve, reject); }, // 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..f98d663a48519 100644 --- a/packages/shared/ReactLazyComponent.js +++ b/packages/shared/ReactLazyComponent.js @@ -7,16 +7,18 @@ * @flow */ -export type Thenable = { +export type LazyComponentThenable = { + $$typeof: Symbol | number, then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, - _reactStatus?: 0 | 1 | 2, - _reactResult: any, + _status: 0 | 1 | 2, + _result: any, }; -type ResolvedThenable = { +type ResolvedLazyComponentThenable = { + $$typeof: Symbol | number, then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, - _reactStatus?: 1, - _reactResult: T, + _status: 1, + _result: any, }; export const Pending = 0; @@ -24,13 +26,13 @@ export const Resolved = 1; export const Rejected = 2; export function getResultFromResolvedThenable( - thenable: ResolvedThenable, + thenable: ResolvedLazyComponentThenable, ): T { - return thenable._reactResult; + return thenable._result; } export function refineResolvedThenable( - thenable: Thenable, -): ResolvedThenable | null { - return thenable._reactStatus === Resolved ? thenable._reactResult : null; + thenable: LazyComponentThenable, +): ResolvedLazyComponentThenable | null { + return thenable._status === Resolved ? thenable._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..7185017e5899f 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactLazyComponent'; +import type {LazyComponentThenable} from 'shared/ReactLazyComponent'; import warningWithoutStack from 'shared/warningWithoutStack'; import { @@ -21,6 +21,7 @@ import { REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, + REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; import {refineResolvedThenable} from 'shared/ReactLazyComponent'; @@ -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: LazyComponentThenable = (type: any); + const resolvedThenable = refineResolvedThenable(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 ||