Skip to content

Commit 7329ea8

Browse files
hansottowirtzeps1lonacdlite
authored
Fix suspense replaying forward refs (#26535)
Continuation of #26420 Fixes #26385 and #26419 --------- Co-authored-by: eps1lon <silbermann.sebastian@gmail.com> Co-authored-by: Andrew Clark <git@andrewclark.io>
1 parent 0ae3480 commit 7329ea8

File tree

3 files changed

+154
-13
lines changed

3 files changed

+154
-13
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,18 +1168,13 @@ export function replayFunctionComponent(
11681168
workInProgress: Fiber,
11691169
nextProps: any,
11701170
Component: any,
1171+
secondArg: any,
11711172
renderLanes: Lanes,
11721173
): Fiber | null {
11731174
// This function is used to replay a component that previously suspended,
11741175
// after its data resolves. It's a simplified version of
11751176
// updateFunctionComponent that reuses the hooks from the previous attempt.
11761177

1177-
let context;
1178-
if (!disableLegacyContext) {
1179-
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1180-
context = getMaskedContext(workInProgress, unmaskedContext);
1181-
}
1182-
11831178
prepareToReadContext(workInProgress, renderLanes);
11841179
if (enableSchedulingProfiler) {
11851180
markComponentRenderStarted(workInProgress);
@@ -1189,7 +1184,7 @@ export function replayFunctionComponent(
11891184
workInProgress,
11901185
Component,
11911186
nextProps,
1192-
context,
1187+
secondArg,
11931188
);
11941189
const hasId = checkDidRenderIdHook();
11951190
if (enableSchedulingProfiler) {

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
enableTransitionTracing,
4040
useModernStrictMode,
4141
revertRemovalOfSiblingPrerendering,
42+
disableLegacyContext,
4243
} from 'shared/ReactFeatureFlags';
4344
import ReactSharedInternals from 'shared/ReactSharedInternals';
4445
import is from 'shared/objectIs';
@@ -281,6 +282,7 @@ import {
281282
flushSyncWorkOnLegacyRootsOnly,
282283
getContinuationForRoot,
283284
} from './ReactFiberRootScheduler';
285+
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
284286

285287
const ceil = Math.ceil;
286288

@@ -2380,8 +2382,8 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
23802382
// Fallthrough to the next branch.
23812383
}
23822384
// eslint-disable-next-line no-fallthrough
2383-
case FunctionComponent:
2384-
case ForwardRef: {
2385+
case SimpleMemoComponent:
2386+
case FunctionComponent: {
23852387
// Resolve `defaultProps`. This logic is copied from `beginWork`.
23862388
// TODO: Consider moving this switch statement into that module. Also,
23872389
// could maybe use this as an opportunity to say `use` doesn't work with
@@ -2392,23 +2394,39 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
23922394
unitOfWork.elementType === Component
23932395
? unresolvedProps
23942396
: resolveDefaultProps(Component, unresolvedProps);
2397+
let context: any;
2398+
if (!disableLegacyContext) {
2399+
const unmaskedContext = getUnmaskedContext(unitOfWork, Component, true);
2400+
context = getMaskedContext(unitOfWork, unmaskedContext);
2401+
}
23952402
next = replayFunctionComponent(
23962403
current,
23972404
unitOfWork,
23982405
resolvedProps,
23992406
Component,
2407+
context,
24002408
workInProgressRootRenderLanes,
24012409
);
24022410
break;
24032411
}
2404-
case SimpleMemoComponent: {
2405-
const Component = unitOfWork.type;
2406-
const nextProps = unitOfWork.pendingProps;
2412+
case ForwardRef: {
2413+
// Resolve `defaultProps`. This logic is copied from `beginWork`.
2414+
// TODO: Consider moving this switch statement into that module. Also,
2415+
// could maybe use this as an opportunity to say `use` doesn't work with
2416+
// `defaultProps` :)
2417+
const Component = unitOfWork.type.render;
2418+
const unresolvedProps = unitOfWork.pendingProps;
2419+
const resolvedProps =
2420+
unitOfWork.elementType === Component
2421+
? unresolvedProps
2422+
: resolveDefaultProps(Component, unresolvedProps);
2423+
24072424
next = replayFunctionComponent(
24082425
current,
24092426
unitOfWork,
2410-
nextProps,
2427+
resolvedProps,
24112428
Component,
2429+
unitOfWork.ref,
24122430
workInProgressRootRenderLanes,
24132431
);
24142432
break;

packages/react-reconciler/src/__tests__/ReactUse-test.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,4 +1472,132 @@ describe('ReactUse', () => {
14721472
assertLog(['Hi']);
14731473
expect(root).toMatchRenderedOutput('Hi');
14741474
});
1475+
1476+
test('unwrap uncached promises inside forwardRef', async () => {
1477+
const asyncInstance = {};
1478+
const Async = React.forwardRef((props, ref) => {
1479+
React.useImperativeHandle(ref, () => asyncInstance);
1480+
const text = use(Promise.resolve('Async'));
1481+
return <Text text={text} />;
1482+
});
1483+
1484+
const ref = React.createRef();
1485+
function App() {
1486+
return (
1487+
<Suspense fallback={<Text text="Loading..." />}>
1488+
<Async ref={ref} />
1489+
</Suspense>
1490+
);
1491+
}
1492+
1493+
const root = ReactNoop.createRoot();
1494+
await act(() => {
1495+
startTransition(() => {
1496+
root.render(<App />);
1497+
});
1498+
});
1499+
assertLog(['Async']);
1500+
expect(root).toMatchRenderedOutput('Async');
1501+
expect(ref.current).toBe(asyncInstance);
1502+
});
1503+
1504+
test('unwrap uncached promises inside memo', async () => {
1505+
const Async = React.memo(
1506+
props => {
1507+
const text = use(Promise.resolve(props.text));
1508+
return <Text text={text} />;
1509+
},
1510+
(a, b) => a.text === b.text,
1511+
);
1512+
1513+
function App({text}) {
1514+
return (
1515+
<Suspense fallback={<Text text="Loading..." />}>
1516+
<Async text={text} />
1517+
</Suspense>
1518+
);
1519+
}
1520+
1521+
const root = ReactNoop.createRoot();
1522+
await act(() => {
1523+
startTransition(() => {
1524+
root.render(<App text="Async" />);
1525+
});
1526+
});
1527+
assertLog(['Async']);
1528+
expect(root).toMatchRenderedOutput('Async');
1529+
1530+
// Update to the same value
1531+
await act(() => {
1532+
startTransition(() => {
1533+
root.render(<App text="Async" />);
1534+
});
1535+
});
1536+
// Should not have re-rendered, because it's memoized
1537+
assertLog([]);
1538+
expect(root).toMatchRenderedOutput('Async');
1539+
1540+
// Update to a different value
1541+
await act(() => {
1542+
startTransition(() => {
1543+
root.render(<App text="Async!" />);
1544+
});
1545+
});
1546+
assertLog(['Async!']);
1547+
expect(root).toMatchRenderedOutput('Async!');
1548+
});
1549+
1550+
// @gate !disableLegacyContext
1551+
test('unwrap uncached promises in component that accesses legacy context', async () => {
1552+
class ContextProvider extends React.Component {
1553+
static childContextTypes = {
1554+
legacyContext() {},
1555+
};
1556+
getChildContext() {
1557+
return {legacyContext: 'Async'};
1558+
}
1559+
render() {
1560+
return this.props.children;
1561+
}
1562+
}
1563+
1564+
function Async({label}, context) {
1565+
const text = use(Promise.resolve(context.legacyContext + ` (${label})`));
1566+
return <Text text={text} />;
1567+
}
1568+
Async.contextTypes = {
1569+
legacyContext: () => {},
1570+
};
1571+
1572+
const AsyncMemo = React.memo(Async, (a, b) => a.label === b.label);
1573+
1574+
function App() {
1575+
return (
1576+
<ContextProvider>
1577+
<Suspense fallback={<Text text="Loading..." />}>
1578+
<div>
1579+
<Async label="function component" />
1580+
</div>
1581+
<div>
1582+
<AsyncMemo label="memo component" />
1583+
</div>
1584+
</Suspense>
1585+
</ContextProvider>
1586+
);
1587+
}
1588+
1589+
const root = ReactNoop.createRoot();
1590+
await act(() => {
1591+
startTransition(() => {
1592+
root.render(<App />);
1593+
});
1594+
});
1595+
assertLog(['Async (function component)', 'Async (memo component)']);
1596+
expect(root).toMatchRenderedOutput(
1597+
<>
1598+
<div>Async (function component)</div>
1599+
<div>Async (memo component)</div>
1600+
</>,
1601+
);
1602+
});
14751603
});

0 commit comments

Comments
 (0)