[Fiber] Treat unwrapping React.lazy more like a use() (#34031)

While we want to get rid of React.lazy's special wrapper type and just
use a Promise for the type, we still have the wrapper.

However, this is still conceptually the same as a Usable in that it
should be have the same if you `use(promise)` or render a Promise as a
child or type position.

This PR makes it behave like a `use()` when we unwrap them. We could
move to a model where it actually reaches the internal of the Lazy's
Promise when it unwraps but for now I leave the lazy API signature
intact by just catching the Promise and then "use()" that.

This lets us align on the semantics with `use()` such as the suspense
yield optimization. It also lets us warn or fork based on legacy
throw-a-Promise behavior where as `React.lazy` is not deprecated.
This commit is contained in:
Sebastian Markbåge 2025-07-29 11:50:12 -04:00 committed by GitHub
parent b1cbb482d5
commit 9be531cd37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 77 deletions

View File

@ -68,9 +68,9 @@ import {
SuspenseActionException,
createThenableState,
trackUsedThenable,
resolveLazy,
} from './ReactFiberThenable';
import {readContextDuringReconciliation} from './ReactFiberNewContext';
import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
import {runWithFiberInDEV} from './ReactCurrentFiber';
@ -364,15 +364,6 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
}
}
function resolveLazy(lazyType: any) {
if (__DEV__) {
return callLazyInitInDEV(lazyType);
}
const payload = lazyType._payload;
const init = lazyType._init;
return init(payload);
}
type ChildReconciler = (
returnFiber: Fiber,
currentFirstChild: Fiber | null,
@ -698,14 +689,7 @@ function createChildReconciler(
}
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((newChild: any));
const created = createChild(returnFiber, resolvedChild, lanes);
currentDebugInfo = prevDebugInfo;
return created;
@ -830,14 +814,7 @@ function createChildReconciler(
}
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((newChild: any));
const updated = updateSlot(
returnFiber,
oldFiber,
@ -962,14 +939,7 @@ function createChildReconciler(
}
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((newChild: any));
const updated = updateFromMap(
existingChildren,
returnFiber,
@ -1086,14 +1056,7 @@ function createChildReconciler(
});
break;
case REACT_LAZY_TYPE: {
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV((child: any));
} else {
const payload = child._payload;
const init = (child._init: any);
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((child: any));
warnOnInvalidKey(
returnFiber,
workInProgress,
@ -1809,14 +1772,7 @@ function createChildReconciler(
);
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let result;
if (__DEV__) {
result = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
result = init(payload);
}
const result = resolveLazy((newChild: any));
const firstChild = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,

View File

@ -302,11 +302,8 @@ import {
pushRootMarkerInstance,
TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent';
import {
callLazyInitInDEV,
callComponentInDEV,
callRenderInDEV,
} from './ReactFiberCallUserSpace';
import {callComponentInDEV, callRenderInDEV} from './ReactFiberCallUserSpace';
import {resolveLazy} from './ReactFiberThenable';
// A special exception that's used to unwind the stack when an update flows
// into a dehydrated boundary.
@ -2020,14 +2017,7 @@ function mountLazyComponent(
const props = workInProgress.pendingProps;
const lazyComponent: LazyComponentType<any, any> = elementType;
let Component;
if (__DEV__) {
Component = callLazyInitInDEV(lazyComponent);
} else {
const payload = lazyComponent._payload;
const init = lazyComponent._init;
Component = init(payload);
}
let Component = resolveLazy(lazyComponent);
// Store the unwrapped component in the type.
workInProgress.type = Component;

View File

@ -14,6 +14,10 @@ import type {
RejectedThenable,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@ -260,6 +264,27 @@ export function suspendCommit(): void {
throw SuspenseyCommitException;
}
export function resolveLazy<T>(lazyType: LazyComponentType<T, any>): T {
try {
if (__DEV__) {
return callLazyInitInDEV(lazyType);
}
const payload = lazyType._payload;
const init = lazyType._init;
return init(payload);
} catch (x) {
if (x !== null && typeof x === 'object' && typeof x.then === 'function') {
// This lazy Suspended. Treat this as if we called use() to unwrap it.
suspendedThenable = x;
if (__DEV__) {
needsToResetSuspendedThenableDEV = true;
}
throw SuspenseException;
}
throw x;
}
}
// This is used to track the actual thenable that suspended so it can be
// passed to the rest of the Suspense implementation — which, for historical
// reasons, expects to receive a thenable.

View File

@ -198,10 +198,7 @@ describe('ReactLazy', () => {
await resolveFakeImport(Foo);
await waitForAll([
'Foo',
...(gate('alwaysThrottleRetries') ? [] : ['Foo']),
]);
await waitForAll(['Foo']);
expect(root).not.toMatchRenderedOutput('FooBar');
await act(() => resolveFakeImport(Bar));
@ -1329,11 +1326,7 @@ describe('ReactLazy', () => {
expect(ref.current).toBe(null);
await act(() => resolveFakeImport(Foo));
assertLog([
'Foo',
// pre-warming
'Foo',
]);
assertLog(['Foo']);
await act(() => resolveFakeImport(ForwardRefBar));
assertLog(['Foo', 'forwardRef', 'Bar']);
@ -1493,11 +1486,7 @@ describe('ReactLazy', () => {
expect(root).not.toMatchRenderedOutput('AB');
await act(() => resolveFakeImport(ChildA));
assertLog([
'A',
// pre-warming
'A',
]);
assertLog(['A']);
await act(() => resolveFakeImport(ChildB));
assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);