@samitouri / QOS-React-2 / commits / d4f58c3b81

Support Promise as a renderable node (#25634)

Implements Promise as a valid React node types. The idea is that any type that can be unwrapped with `use` should also be renderable. When the reconciler encounters a Usable in a child position, it will transparently unwrap the value before reconciling it. The value of the inner value will determine the identity of the child during reconciliation, not the Usable object that wraps around it. Unlike `use`, the reconciler will recursively unwrap the value until it reaches a non-Usable type, e.g. `Usable<Usable<Usable<T>>>` will resolve to T. In this initial commit, I've added support for Promises. I will do Context in the [next step](https://github.com/facebook/react/pull/25641). Being able to render a promise as a child has several interesting implications. The Server Components response format can use this feature in its implementation — instead of wrapping references to client components in `React.lazy`, it can just use a promise. This also fulfills one of the requirements for async components on the client, because an async component always returns a promise for a React node. However, we will likely warn and/or lint against this for the time being because there are major caveats if you re-render an async component in response to user input. (Note: async components already work in a Server Components environment — the caveats only apply to running them in the browser.) To suspend, React uses the same algorithm as `use`: by throwing an exception to unwind the stack, then replaying the begin phase once the promise resolves. It's a little weird to suspend during reconciliation, however, `lazy` already does this so if there were any obvious bugs related to that we likely would have already found them. Still, the structure is a bit unfortunate. Ideally, we shouldn't need to replay the entire begin phase of the parent fiber in order to reconcile the children again. This would require a somewhat significant refactor, because reconciliation happens deep within the begin phase, and depending on the type of work, not always at the end. We should consider as a future improvement.

Andrew Clark committed Mar 11, 2023 at 17:11 UTC d4f58c3b8123f1654ca1b06692c3165928bef8df
6 files changed +404 -21
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+18
@@ -5402,6 +5402,24 @@ describe('ReactDOMFizzServer', () => {
5402 });
5403 expect(getVisibleChildren(container)).toEqual('Hi');
5404 });
5405 +
5406 + it('promise as node', async () => {
5407 + const promise = Promise.resolve('Hi');
5408 + await act(async () => {
5409 + const {pipe} = renderToPipeableStream(promise);
5410 + pipe(writable);
5411 + });
5412 +
5413 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5414 + // automatically. We can't use the same `act` we use for Fiber tests
5415 + // because that relies on the mock Scheduler. Doesn't affect any public
5416 + // API but we might want to fix this for our own internal tests.
5417 + await act(async () => {
5418 + await promise;
5419 + });
5420 +
5421 + expect(getVisibleChildren(container)).toEqual('Hi');
5422 + });
5423 });
5424
5425 describe('useEffectEvent', () => {
packages/react-reconciler/src/ReactChildFiber.js
+136 -3
@@ -8,9 +8,10 @@
8 */
9
10 import type {ReactElement} from 'shared/ReactElementType';
11 -import type {ReactPortal} from 'shared/ReactTypes';
11 +import type {ReactPortal, Thenable} from 'shared/ReactTypes';
12 import type {Fiber} from './ReactInternalTypes';
13 import type {Lanes} from './ReactFiberLane';
14 +import type {ThenableState} from './ReactFiberThenable';
15
16 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
17 import {
@@ -25,6 +26,8 @@ import {
26 REACT_FRAGMENT_TYPE,
27 REACT_PORTAL_TYPE,
28 REACT_LAZY_TYPE,
29 + REACT_CONTEXT_TYPE,
30 + REACT_SERVER_CONTEXT_TYPE,
31 } from 'shared/ReactSymbols';
32 import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
33 import isArray from 'shared/isArray';
@@ -41,6 +44,11 @@ import {
44 import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
45 import {getIsHydrating} from './ReactFiberHydrationContext';
46 import {pushTreeFork} from './ReactFiberTreeContext';
47 +import {createThenableState, trackUsedThenable} from './ReactFiberThenable';
48 +
49 +// This tracks the thenables that are unwrapped during reconcilation.
50 +let thenableState: ThenableState | null = null;
51 +let thenableIndexCounter: number = 0;
52
53 let didWarnAboutMaps;
54 let didWarnAboutGenerators;
@@ -99,6 +107,15 @@ function isReactClass(type: any) {
107 return type.prototype && type.prototype.isReactComponent;
108 }
109
110 +function unwrapThenable<T>(thenable: Thenable<T>): T {
111 + const index = thenableIndexCounter;
112 + thenableIndexCounter += 1;
113 + if (thenableState === null) {
114 + thenableState = createThenableState();
115 + }
116 + return trackUsedThenable(thenableState, thenable, index);
117 +}
118 +
119 function coerceRef(
120 returnFiber: Fiber,
121 current: Fiber | null,
@@ -551,6 +568,21 @@ function createChildReconciler(
568 return created;
569 }
570
571 + // Usable node types
572 + //
573 + // Unwrap the inner value and recursively call this function again.
574 + if (typeof newChild.then === 'function') {
575 + const thenable: Thenable<any> = (newChild: any);
576 + return createChild(returnFiber, unwrapThenable(thenable), lanes);
577 + }
578 +
579 + if (
580 + newChild.$$typeof === REACT_CONTEXT_TYPE ||
581 + newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
582 + ) {
583 + // TODO: Implement Context as child type.
584 + }
585 +
586 throwOnInvalidObjectType(returnFiber, newChild);
587 }
588
@@ -570,7 +602,6 @@ function createChildReconciler(
602 lanes: Lanes,
603 ): Fiber | null {
604 // Update the fiber if the keys match, otherwise return null.
573 -
605 const key = oldFiber !== null ? oldFiber.key : null;
606
607 if (
@@ -617,6 +648,26 @@ function createChildReconciler(
648 return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
649 }
650
651 + // Usable node types
652 + //
653 + // Unwrap the inner value and recursively call this function again.
654 + if (typeof newChild.then === 'function') {
655 + const thenable: Thenable<any> = (newChild: any);
656 + return updateSlot(
657 + returnFiber,
658 + oldFiber,
659 + unwrapThenable(thenable),
660 + lanes,
661 + );
662 + }
663 +
664 + if (
665 + newChild.$$typeof === REACT_CONTEXT_TYPE ||
666 + newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
667 + ) {
668 + // TODO: Implement Context as child type.
669 + }
670 +
671 throwOnInvalidObjectType(returnFiber, newChild);
672 }
673
@@ -679,6 +730,27 @@ function createChildReconciler(
730 return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
731 }
732
733 + // Usable node types
734 + //
735 + // Unwrap the inner value and recursively call this function again.
736 + if (typeof newChild.then === 'function') {
737 + const thenable: Thenable<any> = (newChild: any);
738 + return updateFromMap(
739 + existingChildren,
740 + returnFiber,
741 + newIdx,
742 + unwrapThenable(thenable),
743 + lanes,
744 + );
745 + }
746 +
747 + if (
748 + newChild.$$typeof === REACT_CONTEXT_TYPE ||
749 + newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
750 + ) {
751 + // TODO: Implement Context as child type.
752 + }
753 +
754 throwOnInvalidObjectType(returnFiber, newChild);
755 }
756
@@ -1250,7 +1322,7 @@ function createChildReconciler(
1322 // This API will tag the children with the side-effect of the reconciliation
1323 // itself. They will be added to the side-effect list as we pass through the
1324 // children and the parent.
1253 - function reconcileChildFibers(
1325 + function reconcileChildFibersImpl(
1326 returnFiber: Fiber,
1327 currentFirstChild: Fiber | null,
1328 newChild: any,
@@ -1264,6 +1336,7 @@ function createChildReconciler(
1336 // Handle top level unkeyed fragments as if they were arrays.
1337 // This leads to an ambiguity between <>{[...]}</> and <>...</>.
1338 // We treat the ambiguous cases above the same.
1339 + // TODO: Let's use recursion like we do for Usable nodes?
1340 const isUnkeyedTopLevelFragment =
1341 typeof newChild === 'object' &&
1342 newChild !== null &&
@@ -1324,6 +1397,39 @@ function createChildReconciler(
1397 );
1398 }
1399
1400 + // Usables are a valid React node type. When React encounters a Usable in
1401 + // a child position, it unwraps it using the same algorithm as `use`. For
1402 + // example, for promises, React will throw an exception to unwind the
1403 + // stack, then replay the component once the promise resolves.
1404 + //
1405 + // A difference from `use` is that React will keep unwrapping the value
1406 + // until it reaches a non-Usable type.
1407 + //
1408 + // e.g. Usable<Usable<Usable<T>>> should resolve to T
1409 + //
1410 + // The structure is a bit unfortunate. Ideally, we shouldn't need to
1411 + // replay the entire begin phase of the parent fiber in order to reconcile
1412 + // the children again. This would require a somewhat significant refactor,
1413 + // because reconcilation happens deep within the begin phase, and
1414 + // depending on the type of work, not always at the end. We should
1415 + // consider as an future improvement.
1416 + if (typeof newChild.then === 'function') {
1417 + const thenable: Thenable<any> = (newChild: any);
1418 + return reconcileChildFibersImpl(
1419 + returnFiber,
1420 + currentFirstChild,
1421 + unwrapThenable(thenable),
1422 + lanes,
1423 + );
1424 + }
1425 +
1426 + if (
1427 + newChild.$$typeof === REACT_CONTEXT_TYPE ||
1428 + newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
1429 + ) {
1430 + // TODO: Implement Context as child type.
1431 + }
1432 +
1433 throwOnInvalidObjectType(returnFiber, newChild);
1434 }
1435
@@ -1351,6 +1457,27 @@ function createChildReconciler(
1457 return deleteRemainingChildren(returnFiber, currentFirstChild);
1458 }
1459
1460 + function reconcileChildFibers(
1461 + returnFiber: Fiber,
1462 + currentFirstChild: Fiber | null,
1463 + newChild: any,
1464 + lanes: Lanes,
1465 + ): Fiber | null {
1466 + // This indirection only exists so we can reset `thenableState` at the end.
1467 + // It should get inlined by Closure.
1468 + thenableIndexCounter = 0;
1469 + const firstChildFiber = reconcileChildFibersImpl(
1470 + returnFiber,
1471 + currentFirstChild,
1472 + newChild,
1473 + lanes,
1474 + );
1475 + thenableState = null;
1476 + // Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1477 + // set at the beginning.
1478 + return firstChildFiber;
1479 + }
1480 +
1481 return reconcileChildFibers;
1482 }
1483
@@ -1358,6 +1485,12 @@ export const reconcileChildFibers: ChildReconciler =
1485 createChildReconciler(true);
1486 export const mountChildFibers: ChildReconciler = createChildReconciler(false);
1487
1488 +export function resetChildReconcilerOnUnwind(): void {
1489 + // On unwind, clear any pending thenables that were used.
1490 + thenableState = null;
1491 + thenableIndexCounter = 0;
1492 +}
1493 +
1494 export function cloneChildFibers(
1495 current: Fiber | null,
1496 workInProgress: Fiber,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+10 -8
@@ -278,6 +278,7 @@ import {
278 getShellBoundary,
279 } from './ReactFiberSuspenseContext';
280 import {resolveDefaultProps} from './ReactFiberLazyComponent';
281 +import {resetChildReconcilerOnUnwind} from './ReactChildFiber';
282
283 const ceil = Math.ceil;
284
@@ -1766,6 +1767,7 @@ function resetSuspendedWorkLoopOnUnwind() {
1767 // Reset module-level state that was set during the render phase.
1768 resetContextDependencies();
1769 resetHooksOnUnwind();
1770 + resetChildReconcilerOnUnwind();
1771 }
1772
1773 function handleThrow(root: FiberRoot, thrownValue: any): void {
@@ -2423,14 +2425,14 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2425 break;
2426 }
2427 default: {
2426 - if (__DEV__) {
2427 - console.error(
2428 - 'Unexpected type of work: %s, Currently only function ' +
2429 - 'components are replayed after suspending. This is a bug in React.',
2430 - unitOfWork.tag,
2431 - );
2432 - }
2433 - resetSuspendedWorkLoopOnUnwind();
2428 + // Other types besides function components are reset completely before
2429 + // being replayed. Currently this only happens when a Usable type is
2430 + // reconciled — the reconciler will suspend.
2431 + //
2432 + // We reset the fiber back to its original state; however, this isn't
2433 + // a full "unwind" because we're going to reuse the promises that were
2434 + // reconciled previously. So it's intentional that we don't call
2435 + // resetSuspendedWorkLoopOnUnwind here.
2436 unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2437 unitOfWork = workInProgress = resetWorkInProgress(
2438 unitOfWork,
packages/react-reconciler/src/__tests__/ReactUse-test.js renamed
+200 -1
@@ -8,6 +8,7 @@ let use;
8 let useDebugValue;
9 let useState;
10 let useMemo;
11 +let useEffect;
12 let Suspense;
13 let startTransition;
14 let cache;
@@ -17,7 +18,7 @@ let waitForPaint;
18 let assertLog;
19 let waitForAll;
20
20 -describe('ReactThenable', () => {
21 +describe('ReactUse', () => {
22 beforeEach(() => {
23 jest.resetModules();
24
@@ -29,6 +30,7 @@ describe('ReactThenable', () => {
30 useDebugValue = React.useDebugValue;
31 useState = React.useState;
32 useMemo = React.useMemo;
33 + useEffect = React.useEffect;
34 Suspense = React.Suspense;
35 startTransition = React.startTransition;
36 cache = React.cache;
@@ -1182,4 +1184,201 @@ describe('ReactThenable', () => {
1184 assertLog(['A1']);
1185 expect(root).toMatchRenderedOutput('A1');
1186 });
1187 +
1188 + test('basic promise as child', async () => {
1189 + const promise = Promise.resolve(<Text text="Hi" />);
1190 + const root = ReactNoop.createRoot();
1191 + await act(() => {
1192 + startTransition(() => {
1193 + root.render(promise);
1194 + });
1195 + });
1196 + assertLog(['Hi']);
1197 + expect(root).toMatchRenderedOutput('Hi');
1198 + });
1199 +
1200 + test('basic async component', async () => {
1201 + async function App() {
1202 + await getAsyncText('Hi');
1203 + return <Text text="Hi" />;
1204 + }
1205 +
1206 + const root = ReactNoop.createRoot();
1207 + await act(() => {
1208 + startTransition(() => {
1209 + root.render(<App />);
1210 + });
1211 + });
1212 + assertLog(['Async text requested [Hi]']);
1213 +
1214 + await act(() => resolveTextRequests('Hi'));
1215 + assertLog([
1216 + // TODO: We shouldn't have to replay the function body again. Skip
1217 + // straight to reconciliation.
1218 + 'Async text requested [Hi]',
1219 + 'Hi',
1220 + ]);
1221 + expect(root).toMatchRenderedOutput('Hi');
1222 + });
1223 +
1224 + test('async child of a non-function component (e.g. a class)', async () => {
1225 + class App extends React.Component {
1226 + async render() {
1227 + const text = await getAsyncText('Hi');
1228 + return <Text text={text} />;
1229 + }
1230 + }
1231 +
1232 + const root = ReactNoop.createRoot();
1233 + await act(async () => {
1234 + startTransition(() => {
1235 + root.render(<App />);
1236 + });
1237 + });
1238 + assertLog(['Async text requested [Hi]']);
1239 +
1240 + await act(async () => resolveTextRequests('Hi'));
1241 + assertLog([
1242 + // TODO: We shouldn't have to replay the render function again. We could
1243 + // skip straight to reconciliation. However, it's not as urgent to fix
1244 + // this for fiber types that aren't function components, so we can special
1245 + // case those in the meantime.
1246 + 'Async text requested [Hi]',
1247 + 'Hi',
1248 + ]);
1249 + expect(root).toMatchRenderedOutput('Hi');
1250 + });
1251 +
1252 + test('async children are recursively unwrapped', async () => {
1253 + // This is a Usable of a Usable. `use` would only unwrap a single level, but
1254 + // when passed as a child, the reconciler recurisvely unwraps until it
1255 + // resolves to a non-Usable value.
1256 + const thenable = {
1257 + then() {},
1258 + status: 'fulfilled',
1259 + value: {
1260 + then() {},
1261 + status: 'fulfilled',
1262 + value: <Text text="Hi" />,
1263 + },
1264 + };
1265 + const root = ReactNoop.createRoot();
1266 + await act(() => {
1267 + root.render(thenable);
1268 + });
1269 + assertLog(['Hi']);
1270 + expect(root).toMatchRenderedOutput('Hi');
1271 + });
1272 +
1273 + test('async children are transparently unwrapped before being reconciled (top level)', async () => {
1274 + function Child({text}) {
1275 + useEffect(() => {
1276 + Scheduler.log(`Mount: ${text}`);
1277 + }, [text]);
1278 + return <Text text={text} />;
1279 + }
1280 +
1281 + async function App({text}) {
1282 + // The child returned by this component is always a promise (async
1283 + // functions always return promises). React should unwrap it and reconcile
1284 + // the result, not the promise itself.
1285 + return <Child text={text} />;
1286 + }
1287 +
1288 + const root = ReactNoop.createRoot();
1289 + await act(() => {
1290 + startTransition(() => {
1291 + root.render(<App text="A" />);
1292 + });
1293 + });
1294 + assertLog(['A', 'Mount: A']);
1295 + expect(root).toMatchRenderedOutput('A');
1296 +
1297 + // Update the child's props. It should not remount.
1298 + await act(() => {
1299 + startTransition(() => {
1300 + root.render(<App text="B" />);
1301 + });
1302 + });
1303 + assertLog(['B', 'Mount: B']);
1304 + expect(root).toMatchRenderedOutput('B');
1305 + });
1306 +
1307 + test('async children are transparently unwrapped before being reconciled (siblings)', async () => {
1308 + function Child({text}) {
1309 + useEffect(() => {
1310 + Scheduler.log(`Mount: ${text}`);
1311 + }, [text]);
1312 + return <Text text={text} />;
1313 + }
1314 +
1315 + const root = ReactNoop.createRoot();
1316 + await act(async () => {
1317 + startTransition(() => {
1318 + root.render(
1319 + <>
1320 + {Promise.resolve(<Child text="A" />)}
1321 + {Promise.resolve(<Child text="B" />)}
1322 + {Promise.resolve(<Child text="C" />)}
1323 + </>,
1324 + );
1325 + });
1326 + });
1327 + assertLog(['A', 'B', 'C', 'Mount: A', 'Mount: B', 'Mount: C']);
1328 + expect(root).toMatchRenderedOutput('ABC');
1329 +
1330 + await act(() => {
1331 + startTransition(() => {
1332 + root.render(
1333 + <>
1334 + {Promise.resolve(<Child text="A" />)}
1335 + {Promise.resolve(<Child text="B" />)}
1336 + {Promise.resolve(<Child text="C" />)}
1337 + </>,
1338 + );
1339 + });
1340 + });
1341 + // Nothing should have remounted
1342 + assertLog(['A', 'B', 'C']);
1343 + expect(root).toMatchRenderedOutput('ABC');
1344 + });
1345 +
1346 + test('async children are transparently unwrapped before being reconciled (siblings, reordered)', async () => {
1347 + function Child({text}) {
1348 + useEffect(() => {
1349 + Scheduler.log(`Mount: ${text}`);
1350 + }, [text]);
1351 + return <Text text={text} />;
1352 + }
1353 +
1354 + const root = ReactNoop.createRoot();
1355 + await act(() => {
1356 + startTransition(() => {
1357 + root.render(
1358 + <>
1359 + {Promise.resolve(<Child key="A" text="A" />)}
1360 + {Promise.resolve(<Child key="B" text="B" />)}
1361 + {Promise.resolve(<Child key="C" text="C" />)}
1362 + </>,
1363 + );
1364 + });
1365 + });
1366 + assertLog(['A', 'B', 'C', 'Mount: A', 'Mount: B', 'Mount: C']);
1367 + expect(root).toMatchRenderedOutput('ABC');
1368 +
1369 + await act(() => {
1370 + startTransition(() => {
1371 + root.render(
1372 + <>
1373 + {Promise.resolve(<Child key="B" text="B" />)}
1374 + {Promise.resolve(<Child key="A" text="A" />)}
1375 + {Promise.resolve(<Child key="C" text="C" />)}
1376 + </>,
1377 + );
1378 + });
1379 + });
1380 + // Nothing should have remounted
1381 + assertLog(['B', 'A', 'C']);
1382 + expect(root).toMatchRenderedOutput('BAC');
1383 + });
1384 });
packages/react-server/src/ReactFizzHooks.js
+10 -9
@@ -584,15 +584,7 @@ function use<T>(usable: Usable<T>): T {
584 if (typeof usable.then === 'function') {
585 // This is a thenable.
586 const thenable: Thenable<T> = (usable: any);
587 -
588 - // Track the position of the thenable within this fiber.
589 - const index = thenableIndexCounter;
590 - thenableIndexCounter += 1;
591 -
592 - if (thenableState === null) {
593 - thenableState = createThenableState();
594 - }
595 - return trackUsedThenable(thenableState, thenable, index);
587 + return unwrapThenable(thenable);
588 } else if (
589 usable.$$typeof === REACT_CONTEXT_TYPE ||
590 usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
@@ -606,6 +598,15 @@ function use<T>(usable: Usable<T>): T {
598 throw new Error('An unsupported type was passed to use(): ' + String(usable));
599 }
600
601 +export function unwrapThenable<T>(thenable: Thenable<T>): T {
602 + const index = thenableIndexCounter;
603 + thenableIndexCounter += 1;
604 + if (thenableState === null) {
605 + thenableState = createThenableState();
606 + }
607 + return trackUsedThenable(thenableState, thenable, index);
608 +}
609 +
610 function unsupportedRefresh() {
611 throw new Error('Cache cannot be refreshed during server rendering.');
612 }
packages/react-server/src/ReactFizzServer.js
+30
@@ -18,6 +18,7 @@ import type {
18 ReactProviderType,
19 OffscreenMode,
20 Wakeable,
21 + Thenable,
22 } from 'shared/ReactTypes';
23 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
24 import type {
@@ -102,6 +103,7 @@ import {
103 currentResponseState,
104 setCurrentResponseState,
105 getThenableStateAfterSuspending,
106 + unwrapThenable,
107 } from './ReactFizzHooks';
108 import {DefaultCacheDispatcher} from './ReactFizzCache';
109 import {getStackByComponentStackNode} from './ReactFizzComponentStack';
@@ -123,6 +125,7 @@ import {
125 REACT_MEMO_TYPE,
126 REACT_PROVIDER_TYPE,
127 REACT_CONTEXT_TYPE,
128 + REACT_SERVER_CONTEXT_TYPE,
129 REACT_SCOPE_TYPE,
130 REACT_OFFSCREEN_TYPE,
131 } from 'shared/ReactSymbols';
@@ -1440,6 +1443,33 @@ function renderNodeDestructiveImpl(
1443 }
1444 }
1445
1446 + // Usables are a valid React node type. When React encounters a Usable in
1447 + // a child position, it unwraps it using the same algorithm as `use`. For
1448 + // example, for promises, React will throw an exception to unwind the
1449 + // stack, then replay the component once the promise resolves.
1450 + //
1451 + // A difference from `use` is that React will keep unwrapping the value
1452 + // until it reaches a non-Usable type.
1453 + //
1454 + // e.g. Usable<Usable<Usable<T>>> should resolve to T
1455 + const maybeUsable: Object = node;
1456 + if (typeof maybeUsable.then === 'function') {
1457 + const thenable: Thenable<ReactNodeList> = (maybeUsable: any);
1458 + return renderNodeDestructiveImpl(
1459 + request,
1460 + task,
1461 + null,
1462 + unwrapThenable(thenable),
1463 + );
1464 + }
1465 +
1466 + if (
1467 + maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
1468 + maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
1469 + ) {
1470 + // TODO: Implement Context as child type.
1471 + }
1472 +
1473 // $FlowFixMe[method-unbinding]
1474 const childString = Object.prototype.toString.call(node);
1475