@samitouri / QOS-React-2 / commits / 7329ea81c1

Fix suspense replaying forward refs (#26535)

Continuation of https://github.com/facebook/react/issues/26420 Fixes https://github.com/facebook/react/issues/26385 and https://github.com/facebook/react/issues/26419 --------- Co-authored-by: eps1lon <silbermann.sebastian@gmail.com> Co-authored-by: Andrew Clark <git@andrewclark.io>

Hans Otto Wirtz committed Apr 3, 2023 at 00:48 UTC 7329ea81c154d40800e30104be40f050e8c2af3e
3 files changed +154 -13
packages/react-reconciler/src/ReactFiberBeginWork.js
+2 -7
@@ -1168,18 +1168,13 @@ export function replayFunctionComponent(
1168 workInProgress: Fiber,
1169 nextProps: any,
1170 Component: any,
1171 + secondArg: any,
1172 renderLanes: Lanes,
1173 ): Fiber | null {
1174 // This function is used to replay a component that previously suspended,
1175 // after its data resolves. It's a simplified version of
1176 // updateFunctionComponent that reuses the hooks from the previous attempt.
1177
1177 - let context;
1178 - if (!disableLegacyContext) {
1179 - const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1180 - context = getMaskedContext(workInProgress, unmaskedContext);
1181 - }
1182 -
1178 prepareToReadContext(workInProgress, renderLanes);
1179 if (enableSchedulingProfiler) {
1180 markComponentRenderStarted(workInProgress);
@@ -1189,7 +1184,7 @@ export function replayFunctionComponent(
1184 workInProgress,
1185 Component,
1186 nextProps,
1192 - context,
1187 + secondArg,
1188 );
1189 const hasId = checkDidRenderIdHook();
1190 if (enableSchedulingProfiler) {
packages/react-reconciler/src/ReactFiberWorkLoop.js
+24 -6
@@ -39,6 +39,7 @@ import {
39 enableTransitionTracing,
40 useModernStrictMode,
41 revertRemovalOfSiblingPrerendering,
42 + disableLegacyContext,
43 } from 'shared/ReactFeatureFlags';
44 import ReactSharedInternals from 'shared/ReactSharedInternals';
45 import is from 'shared/objectIs';
@@ -281,6 +282,7 @@ import {
282 flushSyncWorkOnLegacyRootsOnly,
283 getContinuationForRoot,
284 } from './ReactFiberRootScheduler';
285 +import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
286
287 const ceil = Math.ceil;
288
@@ -2380,8 +2382,8 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2382 // Fallthrough to the next branch.
2383 }
2384 // eslint-disable-next-line no-fallthrough
2383 - case FunctionComponent:
2384 - case ForwardRef: {
2385 + case SimpleMemoComponent:
2386 + case FunctionComponent: {
2387 // Resolve `defaultProps`. This logic is copied from `beginWork`.
2388 // TODO: Consider moving this switch statement into that module. Also,
2389 // could maybe use this as an opportunity to say `use` doesn't work with
@@ -2392,23 +2394,39 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2394 unitOfWork.elementType === Component
2395 ? unresolvedProps
2396 : resolveDefaultProps(Component, unresolvedProps);
2397 + let context: any;
2398 + if (!disableLegacyContext) {
2399 + const unmaskedContext = getUnmaskedContext(unitOfWork, Component, true);
2400 + context = getMaskedContext(unitOfWork, unmaskedContext);
2401 + }
2402 next = replayFunctionComponent(
2403 current,
2404 unitOfWork,
2405 resolvedProps,
2406 Component,
2407 + context,
2408 workInProgressRootRenderLanes,
2409 );
2410 break;
2411 }
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 +
2424 next = replayFunctionComponent(
2425 current,
2426 unitOfWork,
2410 - nextProps,
2427 + resolvedProps,
2428 Component,
2429 + unitOfWork.ref,
2430 workInProgressRootRenderLanes,
2431 );
2432 break;
packages/react-reconciler/src/__tests__/ReactUse-test.js
+128
@@ -1472,4 +1472,132 @@ describe('ReactUse', () => {
1472 assertLog(['Hi']);
1473 expect(root).toMatchRenderedOutput('Hi');
1474 });
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 + });
1603 });