@samitouri / QOS-React-2 / commits / 131768166b

Support Context as renderable node (#25641)

## Based on #25634 Like promises, this adds support for Context as a React node. In this initial implementation, the context dependency is added to the parent of child node. This allows the parent to re-reconcile its children when the context updates, so that it can delete the old node if the identity of the child has changed (i.e. if the key or type of an element has changed). But it also means that the parent will replay its entire begin phase. Ideally React would delete the old node and mount the new node without reconciling all the children. I'll leave this for a future optimization.

Andrew Clark committed Mar 11, 2023 at 17:34 UTC 131768166b60b3bc271b54a3f93f011f310519de
5 files changed +192 -9
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+28
@@ -5420,6 +5420,34 @@ describe('ReactDOMFizzServer', () => {
5420
5421 expect(getVisibleChildren(container)).toEqual('Hi');
5422 });
5423 +
5424 + it('context as node', async () => {
5425 + const Context = React.createContext('Hi');
5426 + await act(async () => {
5427 + const {pipe} = renderToPipeableStream(Context);
5428 + pipe(writable);
5429 + });
5430 + expect(getVisibleChildren(container)).toEqual('Hi');
5431 + });
5432 +
5433 + it('recursive Usable as node', async () => {
5434 + const Context = React.createContext('Hi');
5435 + const promiseForContext = Promise.resolve(Context);
5436 + await act(async () => {
5437 + const {pipe} = renderToPipeableStream(promiseForContext);
5438 + pipe(writable);
5439 + });
5440 +
5441 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5442 + // automatically. We can't use the same `act` we use for Fiber tests
5443 + // because that relies on the mock Scheduler. Doesn't affect any public
5444 + // API but we might want to fix this for our own internal tests.
5445 + await act(async () => {
5446 + await promiseForContext;
5447 + });
5448 +
5449 + expect(getVisibleChildren(container)).toEqual('Hi');
5450 + });
5451 });
5452
5453 describe('useEffectEvent', () => {
packages/react-reconciler/src/ReactChildFiber.js
+30 -5
@@ -8,7 +8,7 @@
8 */
9
10 import type {ReactElement} from 'shared/ReactElementType';
11 -import type {ReactPortal, Thenable} from 'shared/ReactTypes';
11 +import type {ReactPortal, Thenable, ReactContext} from 'shared/ReactTypes';
12 import type {Fiber} from './ReactInternalTypes';
13 import type {Lanes} from './ReactFiberLane';
14 import type {ThenableState} from './ReactFiberThenable';
@@ -45,6 +45,7 @@ import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
45 import {getIsHydrating} from './ReactFiberHydrationContext';
46 import {pushTreeFork} from './ReactFiberTreeContext';
47 import {createThenableState, trackUsedThenable} from './ReactFiberThenable';
48 +import {readContextDuringReconcilation} from './ReactFiberNewContext';
49
50 // This tracks the thenables that are unwrapped during reconcilation.
51 let thenableState: ThenableState | null = null;
@@ -580,7 +581,12 @@ function createChildReconciler(
581 newChild.$$typeof === REACT_CONTEXT_TYPE ||
582 newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
583 ) {
583 - // TODO: Implement Context as child type.
584 + const context: ReactContext<mixed> = (newChild: any);
585 + return createChild(
586 + returnFiber,
587 + readContextDuringReconcilation(returnFiber, context, lanes),
588 + lanes,
589 + );
590 }
591
592 throwOnInvalidObjectType(returnFiber, newChild);
@@ -665,7 +671,13 @@ function createChildReconciler(
671 newChild.$$typeof === REACT_CONTEXT_TYPE ||
672 newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
673 ) {
668 - // TODO: Implement Context as child type.
674 + const context: ReactContext<mixed> = (newChild: any);
675 + return updateSlot(
676 + returnFiber,
677 + oldFiber,
678 + readContextDuringReconcilation(returnFiber, context, lanes),
679 + lanes,
680 + );
681 }
682
683 throwOnInvalidObjectType(returnFiber, newChild);
@@ -748,7 +760,14 @@ function createChildReconciler(
760 newChild.$$typeof === REACT_CONTEXT_TYPE ||
761 newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
762 ) {
751 - // TODO: Implement Context as child type.
763 + const context: ReactContext<mixed> = (newChild: any);
764 + return updateFromMap(
765 + existingChildren,
766 + returnFiber,
767 + newIdx,
768 + readContextDuringReconcilation(returnFiber, context, lanes),
769 + lanes,
770 + );
771 }
772
773 throwOnInvalidObjectType(returnFiber, newChild);
@@ -1427,7 +1446,13 @@ function createChildReconciler(
1446 newChild.$$typeof === REACT_CONTEXT_TYPE ||
1447 newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
1448 ) {
1430 - // TODO: Implement Context as child type.
1449 + const context: ReactContext<mixed> = (newChild: any);
1450 + return reconcileChildFibersImpl(
1451 + returnFiber,
1452 + currentFirstChild,
1453 + readContextDuringReconcilation(returnFiber, context, lanes),
1454 + lanes,
1455 + );
1456 }
1457
1458 throwOnInvalidObjectType(returnFiber, newChild);
packages/react-reconciler/src/ReactFiberNewContext.js
+20 -3
@@ -688,7 +688,24 @@ export function readContext<T>(context: ReactContext<T>): T {
688 );
689 }
690 }
691 + return readContextForConsumer(currentlyRenderingFiber, context);
692 +}
693
694 +export function readContextDuringReconcilation<T>(
695 + consumer: Fiber,
696 + context: ReactContext<T>,
697 + renderLanes: Lanes,
698 +): T {
699 + if (currentlyRenderingFiber === null) {
700 + prepareToReadContext(consumer, renderLanes);
701 + }
702 + return readContextForConsumer(consumer, context);
703 +}
704 +
705 +function readContextForConsumer<T>(
706 + consumer: Fiber | null,
707 + context: ReactContext<T>,
708 +): T {
709 const value = isPrimaryRenderer
710 ? context._currentValue
711 : context._currentValue2;
@@ -703,7 +720,7 @@ export function readContext<T>(context: ReactContext<T>): T {
720 };
721
722 if (lastContextDependency === null) {
706 - if (currentlyRenderingFiber === null) {
723 + if (consumer === null) {
724 throw new Error(
725 'Context can only be read while React is rendering. ' +
726 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
@@ -714,12 +731,12 @@ export function readContext<T>(context: ReactContext<T>): T {
731
732 // This is the first dependency for this component. Create a new list.
733 lastContextDependency = contextItem;
717 - currentlyRenderingFiber.dependencies = {
734 + consumer.dependencies = {
735 lanes: NoLanes,
736 firstContext: contextItem,
737 };
738 if (enableLazyContextPropagation) {
722 - currentlyRenderingFiber.flags |= NeedsPropagation;
739 + consumer.flags |= NeedsPropagation;
740 }
741 } else {
742 // Append a new context item.
packages/react-reconciler/src/__tests__/ReactUse-test.js
+107
@@ -1381,4 +1381,111 @@ describe('ReactUse', () => {
1381 assertLog(['B', 'A', 'C']);
1382 expect(root).toMatchRenderedOutput('BAC');
1383 });
1384 +
1385 + test('basic Context as node', async () => {
1386 + const Context = React.createContext(null);
1387 +
1388 + function Indirection({children}) {
1389 + Scheduler.log('Indirection');
1390 + return children;
1391 + }
1392 +
1393 + function ParentOfContextNode() {
1394 + Scheduler.log('ParentOfContextNode');
1395 + return Context;
1396 + }
1397 +
1398 + function Child({text}) {
1399 + useEffect(() => {
1400 + Scheduler.log('Mount');
1401 + return () => {
1402 + Scheduler.log('Unmount');
1403 + };
1404 + }, []);
1405 + return <Text text={text} />;
1406 + }
1407 +
1408 + function App({contextValue, children}) {
1409 + const memoizedChildren = useMemo(
1410 + () => (
1411 + <Indirection>
1412 + <ParentOfContextNode />
1413 + </Indirection>
1414 + ),
1415 + [children],
1416 + );
1417 + return (
1418 + <Context.Provider value={contextValue}>
1419 + {memoizedChildren}
1420 + </Context.Provider>
1421 + );
1422 + }
1423 +
1424 + // Initial render
1425 + const root = ReactNoop.createRoot();
1426 + await act(() => {
1427 + root.render(<App contextValue={<Child text="A" />} />);
1428 + });
1429 + assertLog(['Indirection', 'ParentOfContextNode', 'A', 'Mount']);
1430 + expect(root).toMatchRenderedOutput('A');
1431 +
1432 + // Update the child to a new value
1433 + await act(async () => {
1434 + root.render(<App contextValue={<Child text="B" />} />);
1435 + });
1436 + assertLog([
1437 + // Notice that the <Indirection /> did not rerender, because the
1438 + // update was sent via Context.
1439 +
1440 + // TODO: We shouldn't have to re-render the parent of the context node.
1441 + // This happens because we need to reconcile the parent's children again.
1442 + // However, we should be able to skip directly to reconcilation without
1443 + // evaluating the component. One way to do this might be to mark the
1444 + // context dependency with a flag that says it was added
1445 + // during reconcilation.
1446 + 'ParentOfContextNode',
1447 +
1448 + // Notice that this was an update, not a remount.
1449 + 'B',
1450 + ]);
1451 + expect(root).toMatchRenderedOutput('B');
1452 +
1453 + // Delete the old child and replace it with a new one, by changing the key
1454 + await act(async () => {
1455 + root.render(<App contextValue={<Child key="C" text="C" />} />);
1456 + });
1457 + assertLog([
1458 + 'ParentOfContextNode',
1459 +
1460 + // A new instance is mounted
1461 + 'C',
1462 + 'Unmount',
1463 + 'Mount',
1464 + ]);
1465 + });
1466 +
1467 + test('context as node, at the root', async () => {
1468 + const Context = React.createContext(<Text text="Hi" />);
1469 + const root = ReactNoop.createRoot();
1470 + await act(async () => {
1471 + startTransition(() => {
1472 + root.render(Context);
1473 + });
1474 + });
1475 + assertLog(['Hi']);
1476 + expect(root).toMatchRenderedOutput('Hi');
1477 + });
1478 +
1479 + test('promises that resolves to a context, rendered as a node', async () => {
1480 + const Context = React.createContext(<Text text="Hi" />);
1481 + const promise = Promise.resolve(Context);
1482 + const root = ReactNoop.createRoot();
1483 + await act(async () => {
1484 + startTransition(() => {
1485 + root.render(promise);
1486 + });
1487 + });
1488 + assertLog(['Hi']);
1489 + expect(root).toMatchRenderedOutput('Hi');
1490 + });
1491 });
packages/react-server/src/ReactFizzServer.js
+7 -1
@@ -1467,7 +1467,13 @@ function renderNodeDestructiveImpl(
1467 maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
1468 maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
1469 ) {
1470 - // TODO: Implement Context as child type.
1470 + const context: ReactContext<ReactNodeList> = (maybeUsable: any);
1471 + return renderNodeDestructiveImpl(
1472 + request,
1473 + task,
1474 + null,
1475 + readContext(context),
1476 + );
1477 }
1478
1479 // $FlowFixMe[method-unbinding]