@samitouri / QOS-React / commits / d9659e499e

Lazy components must use React.lazy (#13885)

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.

Andrew Clark committed Oct 18, 2018 at 19:57 UTC d9659e499eba3089b098c32cf112b7a067bdddf1
18 files changed +416 -393
packages/react-dom/src/__tests__/ReactServerRendering-test.js
-5
@@ -582,11 +582,6 @@ describe('ReactDOMServer', () => {
582 );
583 ReactDOMServer.renderToString(<LazyFoo />);
584 }).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
585 -
586 - expect(() => {
587 - const FooPromise = {then() {}};
588 - ReactDOMServer.renderToString(<FooPromise />);
589 - }).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
585 });
586
587 it('should throw (in dev) when children are mutated during render', () => {
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+12 -9
@@ -444,15 +444,18 @@ describe('ReactDOMServerHydration', () => {
444 });
445
446 it('should be able to use lazy components after hydrating', async () => {
447 - const Lazy = new Promise(resolve => {
448 - setTimeout(
449 - () =>
450 - resolve(function World() {
451 - return 'world';
452 - }),
453 - 1000,
454 - );
455 - });
447 + const Lazy = React.lazy(
448 + () =>
449 + new Promise(resolve => {
450 + setTimeout(
451 + () =>
452 + resolve(function World() {
453 + return 'world';
454 + }),
455 + 1000,
456 + );
457 + }),
458 + );
459 class HelloWorld extends React.Component {
460 state = {isClient: false};
461 componentDidMount() {
packages/react-dom/src/server/ReactPartialRenderer.js
+6 -8
@@ -38,6 +38,7 @@ import {
38 REACT_PROFILER_TYPE,
39 REACT_PROVIDER_TYPE,
40 REACT_CONTEXT_TYPE,
41 + REACT_LAZY_TYPE,
42 } from 'shared/ReactSymbols';
43
44 import {
@@ -1005,14 +1006,11 @@ class ReactDOMServerRenderer {
1006 this.stack.push(frame);
1007 return '';
1008 }
1008 - default:
1009 - if (typeof elementType.then === 'function') {
1010 - invariant(
1011 - false,
1012 - 'ReactDOMServer does not yet support lazy-loaded components.',
1013 - );
1014 - }
1015 - break;
1009 + case REACT_LAZY_TYPE:
1010 + invariant(
1011 + false,
1012 + 'ReactDOMServer does not yet support lazy-loaded components.',
1013 + );
1014 }
1015 }
1016
packages/react-reconciler/src/ReactFiber.js
+4 -6
@@ -60,6 +60,7 @@ import {
60 REACT_CONCURRENT_MODE_TYPE,
61 REACT_SUSPENSE_TYPE,
62 REACT_PURE_TYPE,
63 + REACT_LAZY_TYPE,
64 } from 'shared/ReactSymbols';
65
66 let hasBadMapPolyfill;
@@ -461,12 +462,9 @@ export function createFiberFromElement(
462 case REACT_PURE_TYPE:
463 fiberTag = PureComponent;
464 break getTag;
464 - default: {
465 - if (typeof type.then === 'function') {
466 - fiberTag = IndeterminateComponent;
467 - break getTag;
468 - }
469 - }
465 + case REACT_LAZY_TYPE:
466 + fiberTag = IndeterminateComponent;
467 + break getTag;
468 }
469 }
470 let info = '';
packages/react-reconciler/src/ReactFiberBeginWork.js
+8 -7
@@ -104,12 +104,13 @@ import {
104 updateClassInstance,
105 } from './ReactFiberClassComponent';
106 import {readLazyComponentType} from './ReactFiberLazyComponent';
107 -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
107 +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
108 import {
109 resolveLazyComponentTag,
110 createFiberFromFragment,
111 createWorkInProgress,
112 } from './ReactFiber';
113 +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
114
115 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
116
@@ -728,7 +729,7 @@ function mountIndeterminateComponent(
729 if (
730 typeof Component === 'object' &&
731 Component !== null &&
731 - typeof Component.then === 'function'
732 + Component.$$typeof === REACT_LAZY_TYPE
733 ) {
734 // We can't start a User Timing measurement with correct label yet.
735 // Cancel and resume right after we know the tag.
@@ -1422,7 +1423,7 @@ function beginWork(
1423 }
1424 case ClassComponentLazy: {
1425 const thenable = workInProgress.type;
1425 - const Component = getResultFromResolvedThenable(thenable);
1426 + const Component = getResultFromResolvedLazyComponent(thenable);
1427 if (isLegacyContextProvider(Component)) {
1428 pushLegacyContextProvider(workInProgress);
1429 }
@@ -1498,7 +1499,7 @@ function beginWork(
1499 }
1500 case FunctionComponentLazy: {
1501 const thenable = workInProgress.type;
1501 - const Component = getResultFromResolvedThenable(thenable);
1502 + const Component = getResultFromResolvedLazyComponent(thenable);
1503 const unresolvedProps = workInProgress.pendingProps;
1504 const child = updateFunctionComponent(
1505 current,
@@ -1523,7 +1524,7 @@ function beginWork(
1524 }
1525 case ClassComponentLazy: {
1526 const thenable = workInProgress.type;
1526 - const Component = getResultFromResolvedThenable(thenable);
1527 + const Component = getResultFromResolvedLazyComponent(thenable);
1528 const unresolvedProps = workInProgress.pendingProps;
1529 const child = updateClassComponent(
1530 current,
@@ -1565,7 +1566,7 @@ function beginWork(
1566 }
1567 case ForwardRefLazy: {
1568 const thenable = workInProgress.type;
1568 - const Component = getResultFromResolvedThenable(thenable);
1569 + const Component = getResultFromResolvedLazyComponent(thenable);
1570 const unresolvedProps = workInProgress.pendingProps;
1571 const child = updateForwardRef(
1572 current,
@@ -1608,7 +1609,7 @@ function beginWork(
1609 }
1610 case PureComponentLazy: {
1611 const thenable = workInProgress.type;
1611 - const Component = getResultFromResolvedThenable(thenable);
1612 + const Component = getResultFromResolvedLazyComponent(thenable);
1613 const unresolvedProps = workInProgress.pendingProps;
1614 const child = updatePureComponent(
1615 current,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2 -2
@@ -42,7 +42,7 @@ import {
42 } from 'shared/ReactWorkTags';
43 import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
44 import invariant from 'shared/invariant';
45 -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
45 +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
46
47 import {
48 createInstance,
@@ -552,7 +552,7 @@ function completeWork(
552 break;
553 }
554 case ClassComponentLazy: {
555 - const Component = getResultFromResolvedThenable(workInProgress.type);
555 + const Component = getResultFromResolvedLazyComponent(workInProgress.type);
556 if (isLegacyContextProvider(Component)) {
557 popLegacyContext(workInProgress);
558 }
packages/react-reconciler/src/ReactFiberContext.js
+2 -2
@@ -20,7 +20,7 @@ import getComponentName from 'shared/getComponentName';
20 import invariant from 'shared/invariant';
21 import warningWithoutStack from 'shared/warningWithoutStack';
22 import checkPropTypes from 'prop-types/checkPropTypes';
23 -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
23 +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
24
25 import * as ReactCurrentFiber from './ReactCurrentFiber';
26 import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
@@ -298,7 +298,7 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object {
298 break;
299 }
300 case ClassComponentLazy: {
301 - const Component = getResultFromResolvedThenable(node.type);
301 + const Component = getResultFromResolvedLazyComponent(node.type);
302 if (isContextProvider(Component)) {
303 return node.stateNode.__reactInternalMemoizedMergedChildContext;
304 }
packages/react-reconciler/src/ReactFiberLazyComponent.js
+15 -13
@@ -7,26 +7,28 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactLazyComponent';
10 +import type {LazyComponent} from 'shared/ReactLazyComponent';
11
12 import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent';
13
14 -export function readLazyComponentType<T>(thenable: Thenable<T>): T {
15 - const status = thenable._reactStatus;
14 +export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
15 + const status = lazyComponent._status;
16 switch (status) {
17 case Resolved:
18 - const Component: T = thenable._reactResult;
18 + const Component: T = lazyComponent._result;
19 return Component;
20 case Rejected:
21 - throw thenable._reactResult;
21 + throw lazyComponent._result;
22 case Pending:
23 - throw thenable;
23 + throw lazyComponent;
24 default: {
25 - thenable._reactStatus = Pending;
25 + lazyComponent._status = Pending;
26 + const ctor = lazyComponent._ctor;
27 + const thenable = ctor();
28 thenable.then(
29 resolvedValue => {
28 - if (thenable._reactStatus === Pending) {
29 - thenable._reactStatus = Resolved;
30 + if (lazyComponent._status === Pending) {
31 + lazyComponent._status = Resolved;
32 if (typeof resolvedValue === 'object' && resolvedValue !== null) {
33 // If the `default` property is not empty, assume it's the result
34 // of an async import() and use that. Otherwise, use the
@@ -39,13 +41,13 @@ export function readLazyComponentType<T>(thenable: Thenable<T>): T {
41 } else {
42 resolvedValue = resolvedValue;
43 }
42 - thenable._reactResult = resolvedValue;
44 + lazyComponent._result = resolvedValue;
45 }
46 },
47 error => {
46 - if (thenable._reactStatus === Pending) {
47 - thenable._reactStatus = Rejected;
48 - thenable._reactResult = error;
48 + if (lazyComponent._status === Pending) {
49 + lazyComponent._status = Rejected;
50 + lazyComponent._result = error;
51 }
52 },
53 );
packages/react-reconciler/src/ReactFiberReconciler.js
+2 -2
@@ -31,7 +31,7 @@ import {
31 import getComponentName from 'shared/getComponentName';
32 import invariant from 'shared/invariant';
33 import warningWithoutStack from 'shared/warningWithoutStack';
34 -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
34 +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
35
36 import {getPublicInstance} from './ReactFiberHostConfig';
37 import {
@@ -107,7 +107,7 @@ function getContextForSubtree(
107 return processChildContext(fiber, Component, parentContext);
108 }
109 } else if (fiber.tag === ClassComponentLazy) {
110 - const Component = getResultFromResolvedThenable(fiber.type);
110 + const Component = getResultFromResolvedLazyComponent(fiber.type);
111 if (isLegacyContextProvider(Component)) {
112 return processChildContext(fiber, Component, parentContext);
113 }
packages/react-reconciler/src/ReactFiberScheduler.js
+4 -2
@@ -53,7 +53,7 @@ import {
53 import getComponentName from 'shared/getComponentName';
54 import invariant from 'shared/invariant';
55 import warningWithoutStack from 'shared/warningWithoutStack';
56 -import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
56 +import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
57
58 import {
59 scheduleTimeout,
@@ -312,7 +312,9 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
312 break;
313 }
314 case ClassComponentLazy: {
315 - const Component = getResultFromResolvedThenable(failedUnitOfWork.type);
315 + const Component = getResultFromResolvedLazyComponent(
316 + failedUnitOfWork.type,
317 + );
318 if (isLegacyContextProvider(Component)) {
319 popLegacyContext(failedUnitOfWork);
320 }
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js new
+309
@@ -0,0 +1,309 @@
1 +let React;
2 +let ReactTestRenderer;
3 +let ReactFeatureFlags;
4 +let Suspense;
5 +let lazy;
6 +
7 +describe('ReactLazy', () => {
8 + beforeEach(() => {
9 + jest.resetModules();
10 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
11 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
12 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
13 + React = require('react');
14 + Suspense = React.unstable_Suspense;
15 + lazy = React.lazy;
16 + ReactTestRenderer = require('react-test-renderer');
17 + });
18 +
19 + function Text(props) {
20 + ReactTestRenderer.unstable_yield(props.text);
21 + return props.text;
22 + }
23 +
24 + it('suspends until module has loaded', async () => {
25 + const LazyText = lazy(async () => Text);
26 +
27 + const root = ReactTestRenderer.create(
28 + <Suspense fallback={<Text text="Loading..." />}>
29 + <LazyText text="Hi" />
30 + </Suspense>,
31 + {
32 + unstable_isConcurrent: true,
33 + },
34 + );
35 +
36 + expect(root).toFlushAndYield(['Loading...']);
37 + expect(root).toMatchRenderedOutput(null);
38 +
39 + await LazyText;
40 +
41 + expect(root).toFlushAndYield(['Hi']);
42 + expect(root).toMatchRenderedOutput('Hi');
43 +
44 + // Should not suspend on update
45 + root.update(
46 + <Suspense fallback={<Text text="Loading..." />}>
47 + <LazyText text="Hi again" />
48 + </Suspense>,
49 + );
50 + expect(root).toFlushAndYield(['Hi again']);
51 + expect(root).toMatchRenderedOutput('Hi again');
52 + });
53 +
54 + it('uses `default` property, if it exists', async () => {
55 + const LazyText = lazy(async () => ({default: Text}));
56 +
57 + const root = ReactTestRenderer.create(
58 + <Suspense fallback={<Text text="Loading..." />}>
59 + <LazyText text="Hi" />
60 + </Suspense>,
61 + {
62 + unstable_isConcurrent: true,
63 + },
64 + );
65 + expect(root).toFlushAndYield(['Loading...']);
66 + expect(root).toMatchRenderedOutput(null);
67 +
68 + await LazyText;
69 +
70 + expect(root).toFlushAndYield(['Hi']);
71 + expect(root).toMatchRenderedOutput('Hi');
72 +
73 + // Should not suspend on update
74 + root.update(
75 + <Suspense fallback={<Text text="Loading..." />}>
76 + <LazyText text="Hi again" />
77 + </Suspense>,
78 + );
79 + expect(root).toFlushAndYield(['Hi again']);
80 + expect(root).toMatchRenderedOutput('Hi again');
81 + });
82 +
83 + it('throws if promise rejects', async () => {
84 + const LazyText = lazy(async () => {
85 + throw new Error('Bad network');
86 + });
87 +
88 + const root = ReactTestRenderer.create(
89 + <Suspense fallback={<Text text="Loading..." />}>
90 + <LazyText text="Hi" />
91 + </Suspense>,
92 + {
93 + unstable_isConcurrent: true,
94 + },
95 + );
96 +
97 + expect(root).toFlushAndYield(['Loading...']);
98 + expect(root).toMatchRenderedOutput(null);
99 +
100 + try {
101 + await LazyText;
102 + } catch (e) {}
103 +
104 + expect(root).toFlushAndThrow('Bad network');
105 + });
106 +
107 + it('mount and reorder', async () => {
108 + class Child extends React.Component {
109 + componentDidMount() {
110 + ReactTestRenderer.unstable_yield('Did mount: ' + this.props.label);
111 + }
112 + componentDidUpdate() {
113 + ReactTestRenderer.unstable_yield('Did update: ' + this.props.label);
114 + }
115 + render() {
116 + return <Text text={this.props.label} />;
117 + }
118 + }
119 +
120 + const LazyChildA = lazy(async () => Child);
121 + const LazyChildB = lazy(async () => Child);
122 +
123 + function Parent({swap}) {
124 + return (
125 + <Suspense fallback={<Text text="Loading..." />}>
126 + {swap
127 + ? [
128 + <LazyChildB key="B" label="B" />,
129 + <LazyChildA key="A" label="A" />,
130 + ]
131 + : [
132 + <LazyChildA key="A" label="A" />,
133 + <LazyChildB key="B" label="B" />,
134 + ]}
135 + </Suspense>
136 + );
137 + }
138 +
139 + const root = ReactTestRenderer.create(<Parent swap={false} />, {
140 + unstable_isConcurrent: true,
141 + });
142 +
143 + expect(root).toFlushAndYield(['Loading...']);
144 + expect(root).toMatchRenderedOutput(null);
145 +
146 + await LazyChildA;
147 + await LazyChildB;
148 +
149 + expect(root).toFlushAndYield(['A', 'B', 'Did mount: A', 'Did mount: B']);
150 + expect(root).toMatchRenderedOutput('AB');
151 +
152 + // Swap the position of A and B
153 + root.update(<Parent swap={true} />);
154 + expect(root).toFlushAndYield(['B', 'A', 'Did update: B', 'Did update: A']);
155 + expect(root).toMatchRenderedOutput('BA');
156 + });
157 +
158 + it('resolves defaultProps, on mount and update', async () => {
159 + function T(props) {
160 + return <Text {...props} />;
161 + }
162 + T.defaultProps = {text: 'Hi'};
163 + const LazyText = lazy(async () => T);
164 +
165 + const root = ReactTestRenderer.create(
166 + <Suspense fallback={<Text text="Loading..." />}>
167 + <LazyText />
168 + </Suspense>,
169 + {
170 + unstable_isConcurrent: true,
171 + },
172 + );
173 +
174 + expect(root).toFlushAndYield(['Loading...']);
175 + expect(root).toMatchRenderedOutput(null);
176 +
177 + await LazyText;
178 +
179 + expect(root).toFlushAndYield(['Hi']);
180 + expect(root).toMatchRenderedOutput('Hi');
181 +
182 + T.defaultProps = {text: 'Hi again'};
183 + root.update(
184 + <Suspense fallback={<Text text="Loading..." />}>
185 + <LazyText text="Hi again" />
186 + </Suspense>,
187 + );
188 + expect(root).toFlushAndYield(['Hi again']);
189 + expect(root).toMatchRenderedOutput('Hi again');
190 + });
191 +
192 + it('resolves defaultProps without breaking memoization', async () => {
193 + function LazyImpl(props) {
194 + ReactTestRenderer.unstable_yield('Lazy');
195 + return (
196 + <React.Fragment>
197 + <Text text={props.siblingText} />
198 + {props.children}
199 + </React.Fragment>
200 + );
201 + }
202 + LazyImpl.defaultProps = {siblingText: 'Sibling'};
203 + const Lazy = lazy(async () => LazyImpl);
204 +
205 + class Stateful extends React.Component {
206 + state = {text: 'A'};
207 + render() {
208 + return <Text text={this.state.text} />;
209 + }
210 + }
211 +
212 + const stateful = React.createRef(null);
213 +
214 + const root = ReactTestRenderer.create(
215 + <Suspense fallback={<Text text="Loading..." />}>
216 + <Lazy>
217 + <Stateful ref={stateful} />
218 + </Lazy>
219 + </Suspense>,
220 + {
221 + unstable_isConcurrent: true,
222 + },
223 + );
224 + expect(root).toFlushAndYield(['Loading...']);
225 + expect(root).toMatchRenderedOutput(null);
226 +
227 + await Lazy;
228 +
229 + expect(root).toFlushAndYield(['Lazy', 'Sibling', 'A']);
230 + expect(root).toMatchRenderedOutput('SiblingA');
231 +
232 + // Lazy should not re-render
233 + stateful.current.setState({text: 'B'});
234 + expect(root).toFlushAndYield(['B']);
235 + expect(root).toMatchRenderedOutput('SiblingB');
236 + });
237 +
238 + it('includes lazy-loaded component in warning stack', async () => {
239 + const LazyFoo = lazy(() => {
240 + ReactTestRenderer.unstable_yield('Started loading');
241 + const Foo = props => <div>{[<Text text="A" />, <Text text="B" />]}</div>;
242 + return Promise.resolve(Foo);
243 + });
244 +
245 + const root = ReactTestRenderer.create(
246 + <Suspense fallback={<Text text="Loading..." />}>
247 + <LazyFoo />
248 + </Suspense>,
249 + {
250 + unstable_isConcurrent: true,
251 + },
252 + );
253 +
254 + expect(root).toFlushAndYield(['Started loading', 'Loading...']);
255 + expect(root).toMatchRenderedOutput(null);
256 +
257 + await LazyFoo;
258 +
259 + expect(() => {
260 + expect(root).toFlushAndYield(['A', 'B']);
261 + }).toWarnDev(' in Text (at **)\n' + ' in Foo (at **)');
262 + expect(root).toMatchRenderedOutput(<div>AB</div>);
263 + });
264 +
265 + it('supports class and forwardRef components', async () => {
266 + const LazyClass = lazy(async () => {
267 + class Foo extends React.Component {
268 + render() {
269 + return <Text text="Foo" />;
270 + }
271 + }
272 + return Foo;
273 + });
274 +
275 + const LazyForwardRef = lazy(async () => {
276 + class Bar extends React.Component {
277 + render() {
278 + return <Text text="Bar" />;
279 + }
280 + }
281 + return React.forwardRef((props, ref) => {
282 + ReactTestRenderer.unstable_yield('forwardRef');
283 + return <Bar ref={ref} />;
284 + });
285 + });
286 +
287 + const ref = React.createRef();
288 + const root = ReactTestRenderer.create(
289 + <Suspense fallback={<Text text="Loading..." />}>
290 + <LazyClass />
291 + <LazyForwardRef ref={ref} />
292 + </Suspense>,
293 + {
294 + unstable_isConcurrent: true,
295 + },
296 + );
297 +
298 + expect(root).toFlushAndYield(['Loading...']);
299 + expect(root).toMatchRenderedOutput(null);
300 + expect(ref.current).toBe(null);
301 +
302 + await LazyClass;
303 + await LazyForwardRef;
304 +
305 + expect(root).toFlushAndYield(['Foo', 'forwardRef', 'Bar']);
306 + expect(root).toMatchRenderedOutput('FooBar');
307 + expect(ref.current).not.toBe(null);
308 + });
309 +});
packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
+5 -2
@@ -42,9 +42,12 @@ describe('pure', () => {
42 function Indirection(props) {
43 return <Pure {...props} />;
44 }
45 - return Promise.resolve(Indirection);
45 + return React.lazy(async () => Indirection);
46 + });
47 + sharedTests('lazy', (...args) => {
48 + const Pure = React.pure(...args);
49 + return React.lazy(async () => Pure);
50 });
47 - sharedTests('lazy', (...args) => Promise.resolve(React.pure(...args)));
51
52 function sharedTests(label, pure) {
53 describe(`${label}`, () => {
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+6 -296
@@ -6,7 +6,6 @@ let ReactCache;
6 let Suspense;
7 let StrictMode;
8 let ConcurrentMode;
9 -let lazy;
9
10 let cache;
11 let TextResource;
@@ -28,7 +27,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
27 Suspense = React.unstable_Suspense;
28 StrictMode = React.StrictMode;
29 ConcurrentMode = React.unstable_ConcurrentMode;
31 - lazy = React.lazy;
30
31 function invalidateCache() {
32 cache = ReactCache.createCache(invalidateCache);
@@ -50,12 +48,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
48 textResourceShouldFail = false;
49 });
50
53 - function div(...children) {
54 - children = children.map(
55 - c => (typeof c === 'string' ? {text: c, hidden: false} : c),
56 - );
57 - return {type: 'div', children, prop: undefined, hidden: false};
58 - }
51 + // function div(...children) {
52 + // children = children.map(
53 + // c => (typeof c === 'string' ? {text: c, hidden: false} : c),
54 + // );
55 + // return {type: 'div', children, prop: undefined, hidden: false};
56 + // }
57
58 function span(prop) {
59 return {type: 'span', children: [], prop, hidden: false};
@@ -1416,294 +1414,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1414 });
1415 });
1416
1419 - describe('Promise as element type', () => {
1420 - it('accepts a promise as an element type', async () => {
1421 - const LazyText = Promise.resolve(Text);
1422 -
1423 - ReactNoop.render(
1424 - <Suspense fallback={<Text text="Loading..." />}>
1425 - <LazyText text="Hi" />
1426 - </Suspense>,
1427 - );
1428 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1429 - expect(ReactNoop.getChildren()).toEqual([]);
1430 -
1431 - await LazyText;
1432 -
1433 - expect(ReactNoop.flush()).toEqual(['Hi']);
1434 - expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1435 -
1436 - // Should not suspend on update
1437 - ReactNoop.render(
1438 - <Suspense fallback={<Text text="Loading..." />}>
1439 - <LazyText text="Hi again" />
1440 - </Suspense>,
1441 - );
1442 - expect(ReactNoop.flush()).toEqual(['Hi again']);
1443 - expect(ReactNoop.getChildren()).toEqual([span('Hi again')]);
1444 - });
1445 -
1446 - it('throws if promise rejects', async () => {
1447 - const LazyText = Promise.reject(new Error('Bad network'));
1448 -
1449 - ReactNoop.render(
1450 - <Suspense fallback={<Text text="Loading..." />}>
1451 - <LazyText text="Hi" />
1452 - </Suspense>,
1453 - );
1454 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1455 -
1456 - await LazyText.catch(() => {});
1457 -
1458 - expect(() => ReactNoop.flush()).toThrow('Bad network');
1459 - });
1460 -
1461 - it('mount and reorder', async () => {
1462 - class Child extends React.Component {
1463 - componentDidMount() {
1464 - ReactNoop.yield('Did mount: ' + this.props.label);
1465 - }
1466 - componentDidUpdate() {
1467 - ReactNoop.yield('Did update: ' + this.props.label);
1468 - }
1469 - render() {
1470 - return <Text text={this.props.label} />;
1471 - }
1472 - }
1473 -
1474 - const LazyChildA = Promise.resolve(Child);
1475 - const LazyChildB = Promise.resolve(Child);
1476 -
1477 - function Parent({swap}) {
1478 - return (
1479 - <Suspense fallback={<Text text="Loading..." />}>
1480 - {swap
1481 - ? [
1482 - <LazyChildB key="B" label="B" />,
1483 - <LazyChildA key="A" label="A" />,
1484 - ]
1485 - : [
1486 - <LazyChildA key="A" label="A" />,
1487 - <LazyChildB key="B" label="B" />,
1488 - ]}
1489 - </Suspense>
1490 - );
1491 - }
1492 -
1493 - ReactNoop.render(<Parent swap={false} />);
1494 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1495 - expect(ReactNoop.getChildren()).toEqual([]);
1496 -
1497 - await LazyChildA;
1498 - await LazyChildB;
1499 -
1500 - expect(ReactNoop.flush()).toEqual([
1501 - 'A',
1502 - 'B',
1503 - 'Did mount: A',
1504 - 'Did mount: B',
1505 - ]);
1506 - expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1507 -
1508 - // Swap the position of A and B
1509 - ReactNoop.render(<Parent swap={true} />);
1510 - expect(ReactNoop.flush()).toEqual([
1511 - 'B',
1512 - 'A',
1513 - 'Did update: B',
1514 - 'Did update: A',
1515 - ]);
1516 - expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
1517 - });
1518 -
1519 - it('uses `default` property, if it exists', async () => {
1520 - const LazyText = Promise.resolve({default: Text});
1521 -
1522 - ReactNoop.render(
1523 - <Suspense fallback={<Text text="Loading..." />}>
1524 - <LazyText text="Hi" />
1525 - </Suspense>,
1526 - );
1527 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1528 - expect(ReactNoop.getChildren()).toEqual([]);
1529 -
1530 - await LazyText;
1531 -
1532 - expect(ReactNoop.flush()).toEqual(['Hi']);
1533 - expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1534 -
1535 - // Should not suspend on update
1536 - ReactNoop.render(
1537 - <Suspense fallback={<Text text="Loading..." />}>
1538 - <LazyText text="Hi again" />
1539 - </Suspense>,
1540 - );
1541 - expect(ReactNoop.flush()).toEqual(['Hi again']);
1542 - expect(ReactNoop.getChildren()).toEqual([span('Hi again')]);
1543 - });
1544 -
1545 - it('resolves defaultProps, on mount and update', async () => {
1546 - function T(props) {
1547 - return <Text {...props} />;
1548 - }
1549 - T.defaultProps = {text: 'Hi'};
1550 - const LazyText = Promise.resolve(T);
1551 -
1552 - ReactNoop.render(
1553 - <Suspense fallback={<Text text="Loading..." />}>
1554 - <LazyText />
1555 - </Suspense>,
1556 - );
1557 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1558 - expect(ReactNoop.getChildren()).toEqual([]);
1559 -
1560 - await LazyText;
1561 -
1562 - expect(ReactNoop.flush()).toEqual(['Hi']);
1563 - expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1564 -
1565 - T.defaultProps = {text: 'Hi again'};
1566 -
1567 - ReactNoop.render(
1568 - <Suspense fallback={<Text text="Loading..." />}>
1569 - <LazyText text="Hi again" />
1570 - </Suspense>,
1571 - );
1572 - expect(ReactNoop.flush()).toEqual(['Hi again']);
1573 - expect(ReactNoop.getChildren()).toEqual([span('Hi again')]);
1574 - });
1575 -
1576 - it('resolves defaultProps without breaking memoization', async () => {
1577 - function LazyImpl(props) {
1578 - ReactNoop.yield('Lazy');
1579 - return (
1580 - <Fragment>
1581 - <Text text={props.siblingText} />
1582 - {props.children}
1583 - </Fragment>
1584 - );
1585 - }
1586 - LazyImpl.defaultProps = {siblingText: 'Sibling'};
1587 - const Lazy = Promise.resolve(LazyImpl);
1588 -
1589 - class Stateful extends React.Component {
1590 - state = {text: 'A'};
1591 - render() {
1592 - return <Text text={this.state.text} />;
1593 - }
1594 - }
1595 -
1596 - const stateful = React.createRef(null);
1597 - ReactNoop.render(
1598 - <Suspense fallback={<Text text="Loading..." />}>
1599 - <Lazy>
1600 - <Stateful ref={stateful} />
1601 - </Lazy>
1602 - </Suspense>,
1603 - );
1604 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1605 - expect(ReactNoop.getChildren()).toEqual([]);
1606 - await Lazy;
1607 - expect(ReactNoop.flush()).toEqual(['Lazy', 'Sibling', 'A']);
1608 - expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('A')]);
1609 -
1610 - // Lazy should not re-render
1611 - stateful.current.setState({text: 'B'});
1612 - expect(ReactNoop.flush()).toEqual(['B']);
1613 - expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('B')]);
1614 - });
1615 -
1616 - it('lazy-load using React.lazy', async () => {
1617 - const LazyText = lazy(() => {
1618 - ReactNoop.yield('Started loading');
1619 - return Promise.resolve(Text);
1620 - });
1621 -
1622 - ReactNoop.render(
1623 - <Suspense fallback={<Text text="Loading..." />}>
1624 - <Text text="A" />
1625 - <Text text="B" />
1626 - <LazyText text="C" />
1627 - </Suspense>,
1628 - );
1629 - // Render first two siblings. The lazy component should not have
1630 - // started loading yet.
1631 - ReactNoop.flushThrough(['A', 'B']);
1632 -
1633 - // Flush the rest.
1634 - expect(ReactNoop.flush()).toEqual(['Started loading', 'Loading...']);
1635 - expect(ReactNoop.getChildren()).toEqual([]);
1636 -
1637 - await LazyText;
1638 -
1639 - expect(ReactNoop.flush()).toEqual(['A', 'B', 'C']);
1640 - expect(ReactNoop.getChildren()).toEqual([
1641 - span('A'),
1642 - span('B'),
1643 - span('C'),
1644 - ]);
1645 - });
1646 -
1647 - it('includes lazy-loaded component in warning stack', async () => {
1648 - const LazyFoo = lazy(() => {
1649 - ReactNoop.yield('Started loading');
1650 - const Foo = props => (
1651 - <div>{[<Text text="A" />, <Text text="B" />]}</div>
1652 - );
1653 - return Promise.resolve(Foo);
1654 - });
1655 -
1656 - ReactNoop.render(
1657 - <Suspense fallback={<Text text="Loading..." />}>
1658 - <LazyFoo />
1659 - </Suspense>,
1660 - );
1661 - expect(ReactNoop.flush()).toEqual(['Started loading', 'Loading...']);
1662 - expect(ReactNoop.getChildren()).toEqual([]);
1663 -
1664 - await LazyFoo;
1665 - expect(() => {
1666 - expect(ReactNoop.flush()).toEqual(['A', 'B']);
1667 - }).toWarnDev(' in Text (at **)\n' + ' in Foo (at **)');
1668 - expect(ReactNoop.getChildren()).toEqual([div(span('A'), span('B'))]);
1669 - });
1670 -
1671 - it('supports class and forwardRef components', async () => {
1672 - const LazyClass = lazy(() => {
1673 - class Foo extends React.Component {
1674 - render() {
1675 - return <Text text="Foo" />;
1676 - }
1677 - }
1678 - return Promise.resolve(Foo);
1679 - });
1680 -
1681 - const LazyForwardRef = lazy(() => {
1682 - const Bar = React.forwardRef((props, ref) => (
1683 - <Text text="Bar" hostRef={ref} />
1684 - ));
1685 - return Promise.resolve(Bar);
1686 - });
1687 -
1688 - const ref = React.createRef();
1689 - ReactNoop.render(
1690 - <Suspense fallback={<Text text="Loading..." />}>
1691 - <LazyClass />
1692 - <LazyForwardRef ref={ref} />
1693 - </Suspense>,
1694 - );
1695 - expect(ReactNoop.flush()).toEqual(['Loading...']);
1696 - expect(ReactNoop.getChildren()).toEqual([]);
1697 - expect(ref.current).toBe(null);
1698 -
1699 - await LazyClass;
1700 - await LazyForwardRef;
1701 - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
1702 - expect(ReactNoop.getChildren()).toEqual([span('Foo'), span('Bar')]);
1703 - expect(ref.current).not.toBe(null);
1704 - });
1705 - });
1706 -
1417 it('does not call lifecycles of a suspended component', async () => {
1418 class TextWithLifecycle extends React.Component {
1419 componentDidMount() {
packages/react/src/ReactLazy.js
+8 -15
@@ -5,23 +5,16 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -type Thenable<T, R> = {
9 - then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
10 -};
8 +import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
9
12 -export function lazy<T, R>(ctor: () => Thenable<T, R>) {
13 - let thenable = null;
10 +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
11 +
12 +export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
13 return {
15 - then(resolve, reject) {
16 - if (thenable === null) {
17 - // Lazily create thenable by wrapping in an extra thenable.
18 - thenable = ctor();
19 - ctor = null;
20 - }
21 - return thenable.then(resolve, reject);
22 - },
14 + $$typeof: REACT_LAZY_TYPE,
15 + _ctor: ctor,
16 // React uses these fields to store the result.
24 - _reactStatus: -1,
25 - _reactResult: null,
17 + _status: -1,
18 + _result: null,
19 };
20 }
packages/shared/ReactLazyComponent.js
+21 -15
@@ -7,30 +7,36 @@
7 * @flow
8 */
9
10 -export type Thenable<T> = {
11 - then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
12 - _reactStatus?: 0 | 1 | 2,
13 - _reactResult: any,
10 +export type Thenable<T, R> = {
11 + then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
12 };
13
16 -type ResolvedThenable<T> = {
17 - then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
18 - _reactStatus?: 1,
19 - _reactResult: T,
14 +export type LazyComponent<T> = {
15 + $$typeof: Symbol | number,
16 + _ctor: () => Thenable<T, mixed>,
17 + _status: 0 | 1 | 2,
18 + _result: any,
19 +};
20 +
21 +type ResolvedLazyComponentThenable<T> = {
22 + $$typeof: Symbol | number,
23 + _ctor: () => Thenable<T, mixed>,
24 + _status: 1,
25 + _result: any,
26 };
27
28 export const Pending = 0;
29 export const Resolved = 1;
30 export const Rejected = 2;
31
26 -export function getResultFromResolvedThenable<T>(
27 - thenable: ResolvedThenable<T>,
32 +export function getResultFromResolvedLazyComponent<T>(
33 + lazyComponent: ResolvedLazyComponentThenable<T>,
34 ): T {
29 - return thenable._reactResult;
35 + return lazyComponent._result;
36 }
37
32 -export function refineResolvedThenable<T>(
33 - thenable: Thenable<T>,
34 -): ResolvedThenable<T> | null {
35 - return thenable._reactStatus === Resolved ? thenable._reactResult : null;
38 +export function refineResolvedLazyComponent<T>(
39 + lazyComponent: LazyComponent<T>,
40 +): ResolvedLazyComponentThenable<T> | null {
41 + return lazyComponent._status === Resolved ? lazyComponent._result : null;
42 }
packages/shared/ReactSymbols.js
+1
@@ -42,6 +42,7 @@ export const REACT_SUSPENSE_TYPE = hasSymbol
42 ? Symbol.for('react.suspense')
43 : 0xead1;
44 export const REACT_PURE_TYPE = hasSymbol ? Symbol.for('react.pure') : 0xead3;
45 +export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;
46
47 const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
48 const FAUX_ITERATOR_SYMBOL = '@@iterator';
packages/shared/getComponentName.js
+9 -8
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactLazyComponent';
10 +import type {LazyComponent} from 'shared/ReactLazyComponent';
11
12 import warningWithoutStack from 'shared/warningWithoutStack';
13 import {
@@ -21,8 +21,9 @@ import {
21 REACT_PROVIDER_TYPE,
22 REACT_STRICT_MODE_TYPE,
23 REACT_SUSPENSE_TYPE,
24 + REACT_LAZY_TYPE,
25 } from 'shared/ReactSymbols';
25 -import {refineResolvedThenable} from 'shared/ReactLazyComponent';
26 +import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
27
28 function getWrappedName(
29 outerType: mixed,
@@ -80,12 +81,12 @@ function getComponentName(type: mixed): string | null {
81 return getWrappedName(type, type.render, 'ForwardRef');
82 case REACT_PURE_TYPE:
83 return getWrappedName(type, type.render, 'Pure');
83 - }
84 - if (typeof type.then === 'function') {
85 - const thenable: Thenable<mixed> = (type: any);
86 - const resolvedThenable = refineResolvedThenable(thenable);
87 - if (resolvedThenable) {
88 - return getComponentName(resolvedThenable);
84 + case REACT_LAZY_TYPE: {
85 + const thenable: LazyComponent<mixed> = (type: any);
86 + const resolvedThenable = refineResolvedLazyComponent(thenable);
87 + if (resolvedThenable) {
88 + return getComponentName(resolvedThenable);
89 + }
90 }
91 }
92 }
packages/shared/isValidElementType.js
+2 -1
@@ -17,6 +17,7 @@ import {
17 REACT_STRICT_MODE_TYPE,
18 REACT_SUSPENSE_TYPE,
19 REACT_PURE_TYPE,
20 + REACT_LAZY_TYPE,
21 } from 'shared/ReactSymbols';
22
23 export default function isValidElementType(type: mixed) {
@@ -31,7 +32,7 @@ export default function isValidElementType(type: mixed) {
32 type === REACT_SUSPENSE_TYPE ||
33 (typeof type === 'object' &&
34 type !== null &&
34 - (typeof type.then === 'function' ||
35 + (type.$$typeof === REACT_LAZY_TYPE ||
36 type.$$typeof === REACT_PURE_TYPE ||
37 type.$$typeof === REACT_PROVIDER_TYPE ||
38 type.$$typeof === REACT_CONTEXT_TYPE ||