Add Lazy Elements Behind a Flag (#19033)

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.
This commit is contained in:
Sebastian Markbåge 2020-05-28 14:16:35 -07:00 committed by GitHub
parent 4985bb0a80
commit 518ce9c25f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 193 additions and 2 deletions

View File

@ -33,7 +33,11 @@ import {
Block,
} from './ReactWorkTags';
import invariant from 'shared/invariant';
import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags';
import {
warnAboutStringRefs,
enableBlocksAPI,
enableLazyElements,
} from 'shared/ReactFeatureFlags';
import {
createWorkInProgress,
@ -532,6 +536,13 @@ function ChildReconciler(shouldTrackSideEffects) {
created.return = returnFiber;
return created;
}
case REACT_LAZY_TYPE: {
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return createChild(returnFiber, init(payload), lanes);
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
@ -602,6 +613,13 @@ function ChildReconciler(shouldTrackSideEffects) {
return null;
}
}
case REACT_LAZY_TYPE: {
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return updateSlot(returnFiber, oldFiber, init(payload), lanes);
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
@ -663,6 +681,18 @@ function ChildReconciler(shouldTrackSideEffects) {
) || null;
return updatePortal(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return updateFromMap(
existingChildren,
returnFiber,
newIdx,
init(payload),
lanes,
);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
@ -720,6 +750,15 @@ function ChildReconciler(shouldTrackSideEffects) {
key,
);
break;
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = child._payload;
const init = (child._init: any);
warnOnInvalidKey(init(payload), knownKeys, returnFiber);
break;
}
// We intentionally fallthrough here if enableLazyElements is not on.
// eslint-disable-next-lined no-fallthrough
default:
break;
}
@ -1276,6 +1315,18 @@ function ChildReconciler(shouldTrackSideEffects) {
lanes,
),
);
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
}
}

View File

@ -33,7 +33,11 @@ import {
Block,
} from './ReactWorkTags';
import invariant from 'shared/invariant';
import {warnAboutStringRefs, enableBlocksAPI} from 'shared/ReactFeatureFlags';
import {
warnAboutStringRefs,
enableBlocksAPI,
enableLazyElements,
} from 'shared/ReactFeatureFlags';
import {
createWorkInProgress,
@ -542,6 +546,13 @@ function ChildReconciler(shouldTrackSideEffects) {
created.return = returnFiber;
return created;
}
case REACT_LAZY_TYPE: {
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return createChild(returnFiber, init(payload), expirationTime);
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
@ -627,6 +638,18 @@ function ChildReconciler(shouldTrackSideEffects) {
return null;
}
}
case REACT_LAZY_TYPE: {
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return updateSlot(
returnFiber,
oldFiber,
init(payload),
expirationTime,
);
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
@ -709,6 +732,18 @@ function ChildReconciler(shouldTrackSideEffects) {
expirationTime,
);
}
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return updateFromMap(
existingChildren,
returnFiber,
newIdx,
init(payload),
expirationTime,
);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
@ -772,6 +807,15 @@ function ChildReconciler(shouldTrackSideEffects) {
key,
);
break;
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = child._payload;
const init = (child._init: any);
warnOnInvalidKey(init(payload), knownKeys, returnFiber);
break;
}
// We intentionally fallthrough here if enableLazyElements is not on.
// eslint-disable-next-lined no-fallthrough
default:
break;
}
@ -1349,6 +1393,18 @@ function ChildReconciler(shouldTrackSideEffects) {
expirationTime,
),
);
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
expirationTime,
);
}
}
}

View File

@ -1273,4 +1273,80 @@ describe('ReactLazy', () => {
expect(componentStackMessage).toContain('in Lazy');
});
// @gate enableLazyElements && enableNewReconciler
it('mount and reorder lazy elements', async () => {
class Child extends React.Component {
componentDidMount() {
Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
}
componentDidUpdate() {
Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
}
render() {
return <Text text={this.props.label} />;
}
}
const lazyChildA = lazy(() => {
Scheduler.unstable_yieldValue('Init A');
return fakeImport(<Child key="A" label="A" />);
});
const lazyChildB = lazy(() => {
Scheduler.unstable_yieldValue('Init B');
return fakeImport(<Child key="B" label="B" />);
});
const lazyChildA2 = lazy(() => {
Scheduler.unstable_yieldValue('Init A2');
return fakeImport(<Child key="A" label="a" />);
});
const lazyChildB2 = lazy(() => {
Scheduler.unstable_yieldValue('Init B2');
return fakeImport(<Child key="B" label="b" />);
});
function Parent({swap}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
{swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]}
</Suspense>
);
}
const root = ReactTestRenderer.create(<Parent swap={false} />, {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield(['Init A', 'Loading...']);
expect(root).not.toMatchRenderedOutput('AB');
await lazyChildA;
// We need to flush to trigger the B to load.
expect(Scheduler).toFlushAndYield(['Init B']);
await lazyChildB;
expect(Scheduler).toFlushAndYield([
'A',
'B',
'Did mount: A',
'Did mount: B',
]);
expect(root).toMatchRenderedOutput('AB');
// Swap the position of A and B
root.update(<Parent swap={true} />);
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
await lazyChildB2;
// We need to flush to trigger the second one to load.
expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']);
await lazyChildA2;
expect(Scheduler).toFlushAndYield([
'b',
'a',
'Did update: b',
'Did update: a',
]);
expect(root).toMatchRenderedOutput('ba');
});
});

View File

@ -41,6 +41,7 @@ export const enableSelectiveHydration = __EXPERIMENTAL__;
// Flight experiments
export const enableBlocksAPI = __EXPERIMENTAL__;
export const enableLazyElements = __EXPERIMENTAL__;
// Only used in www builds.
export const enableSchedulerDebugging = false;

View File

@ -18,6 +18,7 @@ export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableBlocksAPI = false;
export const enableLazyElements = false;
export const enableSchedulerDebugging = false;
export const debugRenderPhaseSideEffectsForStrictMode = true;
export const disableJavaScriptURLs = false;

View File

@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableBlocksAPI = false;
export const enableLazyElements = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableSchedulerDebugging = false;

View File

@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableBlocksAPI = false;
export const enableLazyElements = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableSchedulerDebugging = false;

View File

@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableBlocksAPI = false;
export const enableLazyElements = false;
export const enableSchedulerDebugging = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;

View File

@ -20,6 +20,7 @@ export const enableSchedulerTracing = __PROFILE__;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableBlocksAPI = false;
export const enableLazyElements = false;
export const disableJavaScriptURLs = false;
export const disableInputAttributeSyncing = false;
export const enableSchedulerDebugging = false;

View File

@ -20,6 +20,7 @@ export const enableSchedulerTracing = false;
export const enableSuspenseServerRenderer = true;
export const enableSelectiveHydration = true;
export const enableBlocksAPI = true;
export const enableLazyElements = false;
export const disableJavaScriptURLs = true;
export const disableInputAttributeSyncing = false;
export const enableSchedulerDebugging = false;

View File

@ -50,6 +50,7 @@ export const enableSuspenseServerRenderer = true;
export const enableSelectiveHydration = true;
export const enableBlocksAPI = true;
export const enableLazyElements = true;
export const disableJavaScriptURLs = true;