Skip to content

Commit 21b624a

Browse files
committed
Add Lazy Elements Behind a Flag
We really needed this for Flight before as well but we got away with it because Blocks were lazy but with the removal of Blocks, we'll need this to ensure that we can lazily stream in part of the content. Luckily LazyComponent isn't really just a Component. It's just a generic type that can resolve into anything kind of like a Promise. So we can use that to resolve elements just like we can components. This allows keys and props to become lazy as well. To accomplish this, we suspend during reconciliation. This causes us to not be able to render siblings because we don't know if the keys will reconcile. For initial render we could probably special case this and just render a lazy component fiber. Throwing in reconciliation didn't work correctly with direct nested siblings of a Suspense boundary before but it does now so it depends on new reconciler.
1 parent 6071486 commit 21b624a

11 files changed

+193
-2
lines changed

packages/react-reconciler/src/ReactChildFiber.new.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import {
3333
Block,
3434
} from './ReactWorkTags';
3535
import invariant from 'shared/invariant';
36-
import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags';
36+
import {
37+
warnAboutStringRefs,
38+
enableBlocksAPI,
39+
enableLazyElements,
40+
} from 'shared/ReactFeatureFlags';
3741

3842
import {
3943
createWorkInProgress,
@@ -532,6 +536,13 @@ function ChildReconciler(shouldTrackSideEffects) {
532536
created.return = returnFiber;
533537
return created;
534538
}
539+
case REACT_LAZY_TYPE: {
540+
if (enableLazyElements) {
541+
const payload = newChild._payload;
542+
const init = newChild._init;
543+
return createChild(returnFiber, init(payload), lanes);
544+
}
545+
}
535546
}
536547

537548
if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -602,6 +613,13 @@ function ChildReconciler(shouldTrackSideEffects) {
602613
return null;
603614
}
604615
}
616+
case REACT_LAZY_TYPE: {
617+
if (enableLazyElements) {
618+
const payload = newChild._payload;
619+
const init = newChild._init;
620+
return updateSlot(returnFiber, oldFiber, init(payload), lanes);
621+
}
622+
}
605623
}
606624

607625
if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -663,6 +681,18 @@ function ChildReconciler(shouldTrackSideEffects) {
663681
) || null;
664682
return updatePortal(returnFiber, matchedFiber, newChild, lanes);
665683
}
684+
case REACT_LAZY_TYPE:
685+
if (enableLazyElements) {
686+
const payload = newChild._payload;
687+
const init = newChild._init;
688+
return updateFromMap(
689+
existingChildren,
690+
returnFiber,
691+
newIdx,
692+
init(payload),
693+
lanes,
694+
);
695+
}
666696
}
667697

668698
if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -720,6 +750,15 @@ function ChildReconciler(shouldTrackSideEffects) {
720750
key,
721751
);
722752
break;
753+
case REACT_LAZY_TYPE:
754+
if (enableLazyElements) {
755+
const payload = child._payload;
756+
const init = (child._init: any);
757+
warnOnInvalidKey(init(payload), knownKeys, returnFiber);
758+
break;
759+
}
760+
// We intentionally fallthrough here if enableLazyElements is not on.
761+
// eslint-disable-next-lined no-fallthrough
723762
default:
724763
break;
725764
}
@@ -1276,6 +1315,18 @@ function ChildReconciler(shouldTrackSideEffects) {
12761315
lanes,
12771316
),
12781317
);
1318+
case REACT_LAZY_TYPE:
1319+
if (enableLazyElements) {
1320+
const payload = newChild._payload;
1321+
const init = newChild._init;
1322+
// TODO: This function is supposed to be non-recursive.
1323+
return reconcileChildFibers(
1324+
returnFiber,
1325+
currentFirstChild,
1326+
init(payload),
1327+
lanes,
1328+
);
1329+
}
12791330
}
12801331
}
12811332

packages/react-reconciler/src/ReactChildFiber.old.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import {
3333
Block,
3434
} from './ReactWorkTags';
3535
import invariant from 'shared/invariant';
36-
import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags';
36+
import {
37+
warnAboutStringRefs,
38+
enableBlocksAPI,
39+
enableLazyElements,
40+
} from 'shared/ReactFeatureFlags';
3741

3842
import {
3943
createWorkInProgress,
@@ -542,6 +546,13 @@ function ChildReconciler(shouldTrackSideEffects) {
542546
created.return = returnFiber;
543547
return created;
544548
}
549+
case REACT_LAZY_TYPE: {
550+
if (enableLazyElements) {
551+
const payload = newChild._payload;
552+
const init = newChild._init;
553+
return createChild(returnFiber, init(payload), expirationTime);
554+
}
555+
}
545556
}
546557

547558
if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -627,6 +638,18 @@ function ChildReconciler(shouldTrackSideEffects) {
627638
return null;
628639
}
629640
}
641+
case REACT_LAZY_TYPE: {
642+
if (enableLazyElements) {
643+
const payload = newChild._payload;
644+
const init = newChild._init;
645+
return updateSlot(
646+
returnFiber,
647+
oldFiber,
648+
init(payload),
649+
expirationTime,
650+
);
651+
}
652+
}
630653
}
631654

632655
if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -709,6 +732,18 @@ function ChildReconciler(shouldTrackSideEffects) {
709732
expirationTime,
710733
);
711734
}
735+
case REACT_LAZY_TYPE:
736+
if (enableLazyElements) {
737+
const payload = newChild._payload;
738+
const init = newChild._init;
739+
return updateFromMap(
740+
existingChildren,
741+
returnFiber,
742+
newIdx,
743+
init(payload),
744+
expirationTime,
745+
);
746+
}
712747
}
713748

714749
if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -772,6 +807,15 @@ function ChildReconciler(shouldTrackSideEffects) {
772807
key,
773808
);
774809
break;
810+
case REACT_LAZY_TYPE:
811+
if (enableLazyElements) {
812+
const payload = child._payload;
813+
const init = (child._init: any);
814+
warnOnInvalidKey(init(payload), knownKeys, returnFiber);
815+
break;
816+
}
817+
// We intentionally fallthrough here if enableLazyElements is not on.
818+
// eslint-disable-next-lined no-fallthrough
775819
default:
776820
break;
777821
}
@@ -1349,6 +1393,18 @@ function ChildReconciler(shouldTrackSideEffects) {
13491393
expirationTime,
13501394
),
13511395
);
1396+
case REACT_LAZY_TYPE:
1397+
if (enableLazyElements) {
1398+
const payload = newChild._payload;
1399+
const init = newChild._init;
1400+
// TODO: This function is supposed to be non-recursive.
1401+
return reconcileChildFibers(
1402+
returnFiber,
1403+
currentFirstChild,
1404+
init(payload),
1405+
expirationTime,
1406+
);
1407+
}
13521408
}
13531409
}
13541410

packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,4 +1273,80 @@ describe('ReactLazy', () => {
12731273

12741274
expect(componentStackMessage).toContain('in Lazy');
12751275
});
1276+
1277+
// @gate enableLazyElements && enableNewReconciler
1278+
it('mount and reorder lazy elements', async () => {
1279+
class Child extends React.Component {
1280+
componentDidMount() {
1281+
Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
1282+
}
1283+
componentDidUpdate() {
1284+
Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
1285+
}
1286+
render() {
1287+
return <Text text={this.props.label} />;
1288+
}
1289+
}
1290+
1291+
const lazyChildA = lazy(() => {
1292+
Scheduler.unstable_yieldValue('Init A');
1293+
return fakeImport(<Child key="A" label="A" />);
1294+
});
1295+
const lazyChildB = lazy(() => {
1296+
Scheduler.unstable_yieldValue('Init B');
1297+
return fakeImport(<Child key="B" label="B" />);
1298+
});
1299+
const lazyChildA2 = lazy(() => {
1300+
Scheduler.unstable_yieldValue('Init A2');
1301+
return fakeImport(<Child key="A" label="a" />);
1302+
});
1303+
const lazyChildB2 = lazy(() => {
1304+
Scheduler.unstable_yieldValue('Init B2');
1305+
return fakeImport(<Child key="B" label="b" />);
1306+
});
1307+
1308+
function Parent({swap}) {
1309+
return (
1310+
<Suspense fallback={<Text text="Loading..." />}>
1311+
{swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]}
1312+
</Suspense>
1313+
);
1314+
}
1315+
1316+
const root = ReactTestRenderer.create(<Parent swap={false} />, {
1317+
unstable_isConcurrent: true,
1318+
});
1319+
1320+
expect(Scheduler).toFlushAndYield(['Init A', 'Loading...']);
1321+
expect(root).not.toMatchRenderedOutput('AB');
1322+
1323+
await lazyChildA;
1324+
// We need to flush to trigger the B to load.
1325+
expect(Scheduler).toFlushAndYield(['Init B']);
1326+
await lazyChildB;
1327+
1328+
expect(Scheduler).toFlushAndYield([
1329+
'A',
1330+
'B',
1331+
'Did mount: A',
1332+
'Did mount: B',
1333+
]);
1334+
expect(root).toMatchRenderedOutput('AB');
1335+
1336+
// Swap the position of A and B
1337+
root.update(<Parent swap={true} />);
1338+
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1339+
await lazyChildB2;
1340+
// We need to flush to trigger the second one to load.
1341+
expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']);
1342+
await lazyChildA2;
1343+
1344+
expect(Scheduler).toFlushAndYield([
1345+
'b',
1346+
'a',
1347+
'Did update: b',
1348+
'Did update: a',
1349+
]);
1350+
expect(root).toMatchRenderedOutput('ba');
1351+
});
12761352
});

packages/shared/ReactFeatureFlags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const enableSelectiveHydration = __EXPERIMENTAL__;
4141

4242
// Flight experiments
4343
export const enableBlocksAPI = __EXPERIMENTAL__;
44+
export const enableLazyElements = __EXPERIMENTAL__;
4445

4546
// Only used in www builds.
4647
export const enableSchedulerDebugging = false;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const enableSchedulerTracing = __PROFILE__;
1818
export const enableSuspenseServerRenderer = false;
1919
export const enableSelectiveHydration = false;
2020
export const enableBlocksAPI = false;
21+
export const enableLazyElements = false;
2122
export const enableSchedulerDebugging = false;
2223
export const debugRenderPhaseSideEffectsForStrictMode = true;
2324
export const disableJavaScriptURLs = false;

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
2020
export const enableSuspenseServerRenderer = false;
2121
export const enableSelectiveHydration = false;
2222
export const enableBlocksAPI = false;
23+
export const enableLazyElements = false;
2324
export const disableJavaScriptURLs = false;
2425
export const disableInputAttributeSyncing = false;
2526
export const enableSchedulerDebugging = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
2020
export const enableSuspenseServerRenderer = false;
2121
export const enableSelectiveHydration = false;
2222
export const enableBlocksAPI = false;
23+
export const enableLazyElements = false;
2324
export const disableJavaScriptURLs = false;
2425
export const disableInputAttributeSyncing = false;
2526
export const enableSchedulerDebugging = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
2020
export const enableSuspenseServerRenderer = false;
2121
export const enableSelectiveHydration = false;
2222
export const enableBlocksAPI = false;
23+
export const enableLazyElements = false;
2324
export const enableSchedulerDebugging = false;
2425
export const disableJavaScriptURLs = false;
2526
export const disableInputAttributeSyncing = false;

packages/shared/forks/ReactFeatureFlags.testing.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
2020
export const enableSuspenseServerRenderer = false;
2121
export const enableSelectiveHydration = false;
2222
export const enableBlocksAPI = false;
23+
export const enableLazyElements = false;
2324
export const disableJavaScriptURLs = false;
2425
export const disableInputAttributeSyncing = false;
2526
export const enableSchedulerDebugging = false;

packages/shared/forks/ReactFeatureFlags.testing.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const enableSchedulerTracing = false;
2020
export const enableSuspenseServerRenderer = true;
2121
export const enableSelectiveHydration = true;
2222
export const enableBlocksAPI = true;
23+
export const enableLazyElements = false;
2324
export const disableJavaScriptURLs = true;
2425
export const disableInputAttributeSyncing = false;
2526
export const enableSchedulerDebugging = false;

0 commit comments

Comments
 (0)