From 518ce9c25f18075214cb572e7dc240598090e648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 28 May 2020 14:16:35 -0700 Subject: [PATCH] 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. --- .../src/ReactChildFiber.new.js | 53 ++++++++++++- .../src/ReactChildFiber.old.js | 58 +++++++++++++- .../src/__tests__/ReactLazy-test.internal.js | 76 +++++++++++++++++++ packages/shared/ReactFeatureFlags.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 11 files changed, 193 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index 5b0e858d6a..0406a362db 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -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, + ); + } } } diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js index 3f5da1699a..1eeaa2d022 100644 --- a/packages/react-reconciler/src/ReactChildFiber.old.js +++ b/packages/react-reconciler/src/ReactChildFiber.old.js @@ -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, + ); + } } } diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 2f79cdf653..202396660b 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -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 ; + } + } + + const lazyChildA = lazy(() => { + Scheduler.unstable_yieldValue('Init A'); + return fakeImport(); + }); + const lazyChildB = lazy(() => { + Scheduler.unstable_yieldValue('Init B'); + return fakeImport(); + }); + const lazyChildA2 = lazy(() => { + Scheduler.unstable_yieldValue('Init A2'); + return fakeImport(); + }); + const lazyChildB2 = lazy(() => { + Scheduler.unstable_yieldValue('Init B2'); + return fakeImport(); + }); + + function Parent({swap}) { + return ( + }> + {swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]} + + ); + } + + const root = ReactTestRenderer.create(, { + 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(); + 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'); + }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index acd8c92088..dae3d1cd06 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index f4277d4c40..5906c46373 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index f71b664b35..ff13b70580 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 8d16a4ba10..fa7f20ca64 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index b2b014065e..11ff1ce113 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 76667d09bb..57fa6b0863 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 5dd0f5b420..7d1fa21c0e 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 82c05a8acc..ad49cb47c9 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -50,6 +50,7 @@ export const enableSuspenseServerRenderer = true; export const enableSelectiveHydration = true; export const enableBlocksAPI = true; +export const enableLazyElements = true; export const disableJavaScriptURLs = true;