From 21b624afc1a55de8738a56ec5dc82439d6f30bc3 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 27 May 2020 23:21:32 -0700 Subject: [PATCH] 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. --- .../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 5b0e858d6af96..0406a362db5b2 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 3f5da1699a93d..1eeaa2d022ee7 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 2f79cdf653ef5..202396660b3fd 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 300bb5c859e0c..31042e493c2bb 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 375e5797627e2..7e72f53dc7e0b 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 c0e300fa17707..2c598645be664 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 5dfa2ebf3e043..c4f35fe9e9f97 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 16a1e8fabedb5..8e299769894ae 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 b034a559fc5e5..5080f0e2647a0 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 ae2ad698c3865..bd5442df091db 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 b9ee3f04086a8..21b9d663da653 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;