Skip to content

Commit d4f58c3

Browse files
authoredMar 11, 2023
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](#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.
1 parent f411e89 commit d4f58c3

File tree

6 files changed

+404
-21
lines changed

6 files changed

+404
-21
lines changed
 

‎packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+18
Original file line numberDiff line numberDiff line change
@@ -5402,6 +5402,24 @@ describe('ReactDOMFizzServer', () => {
54025402
});
54035403
expect(getVisibleChildren(container)).toEqual('Hi');
54045404
});
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+
});
54055423
});
54065424

54075425
describe('useEffectEvent', () => {

‎packages/react-reconciler/src/ReactChildFiber.js

+136-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
*/
99

1010
import type {ReactElement} from 'shared/ReactElementType';
11-
import type {ReactPortal} from 'shared/ReactTypes';
11+
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
1212
import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane';
14+
import type {ThenableState} from './ReactFiberThenable';
1415

1516
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
1617
import {
@@ -25,6 +26,8 @@ import {
2526
REACT_FRAGMENT_TYPE,
2627
REACT_PORTAL_TYPE,
2728
REACT_LAZY_TYPE,
29+
REACT_CONTEXT_TYPE,
30+
REACT_SERVER_CONTEXT_TYPE,
2831
} from 'shared/ReactSymbols';
2932
import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
3033
import isArray from 'shared/isArray';
@@ -41,6 +44,11 @@ import {
4144
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
4245
import {getIsHydrating} from './ReactFiberHydrationContext';
4346
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;
4452

4553
let didWarnAboutMaps;
4654
let didWarnAboutGenerators;
@@ -99,6 +107,15 @@ function isReactClass(type: any) {
99107
return type.prototype && type.prototype.isReactComponent;
100108
}
101109

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+
102119
function coerceRef(
103120
returnFiber: Fiber,
104121
current: Fiber | null,
@@ -551,6 +568,21 @@ function createChildReconciler(
551568
return created;
552569
}
553570

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+
554586
throwOnInvalidObjectType(returnFiber, newChild);
555587
}
556588

@@ -570,7 +602,6 @@ function createChildReconciler(
570602
lanes: Lanes,
571603
): Fiber | null {
572604
// Update the fiber if the keys match, otherwise return null.
573-
574605
const key = oldFiber !== null ? oldFiber.key : null;
575606

576607
if (
@@ -617,6 +648,26 @@ function createChildReconciler(
617648
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
618649
}
619650

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+
620671
throwOnInvalidObjectType(returnFiber, newChild);
621672
}
622673

@@ -679,6 +730,27 @@ function createChildReconciler(
679730
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
680731
}
681732

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+
682754
throwOnInvalidObjectType(returnFiber, newChild);
683755
}
684756

@@ -1250,7 +1322,7 @@ function createChildReconciler(
12501322
// This API will tag the children with the side-effect of the reconciliation
12511323
// itself. They will be added to the side-effect list as we pass through the
12521324
// children and the parent.
1253-
function reconcileChildFibers(
1325+
function reconcileChildFibersImpl(
12541326
returnFiber: Fiber,
12551327
currentFirstChild: Fiber | null,
12561328
newChild: any,
@@ -1264,6 +1336,7 @@ function createChildReconciler(
12641336
// Handle top level unkeyed fragments as if they were arrays.
12651337
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
12661338
// We treat the ambiguous cases above the same.
1339+
// TODO: Let's use recursion like we do for Usable nodes?
12671340
const isUnkeyedTopLevelFragment =
12681341
typeof newChild === 'object' &&
12691342
newChild !== null &&
@@ -1324,6 +1397,39 @@ function createChildReconciler(
13241397
);
13251398
}
13261399

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+
13271433
throwOnInvalidObjectType(returnFiber, newChild);
13281434
}
13291435

@@ -1351,13 +1457,40 @@ function createChildReconciler(
13511457
return deleteRemainingChildren(returnFiber, currentFirstChild);
13521458
}
13531459

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+
13541481
return reconcileChildFibers;
13551482
}
13561483

13571484
export const reconcileChildFibers: ChildReconciler =
13581485
createChildReconciler(true);
13591486
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
13601487

1488+
export function resetChildReconcilerOnUnwind(): void {
1489+
// On unwind, clear any pending thenables that were used.
1490+
thenableState = null;
1491+
thenableIndexCounter = 0;
1492+
}
1493+
13611494
export function cloneChildFibers(
13621495
current: Fiber | null,
13631496
workInProgress: Fiber,

‎packages/react-reconciler/src/ReactFiberWorkLoop.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ import {
278278
getShellBoundary,
279279
} from './ReactFiberSuspenseContext';
280280
import {resolveDefaultProps} from './ReactFiberLazyComponent';
281+
import {resetChildReconcilerOnUnwind} from './ReactChildFiber';
281282

282283
const ceil = Math.ceil;
283284

@@ -1766,6 +1767,7 @@ function resetSuspendedWorkLoopOnUnwind() {
17661767
// Reset module-level state that was set during the render phase.
17671768
resetContextDependencies();
17681769
resetHooksOnUnwind();
1770+
resetChildReconcilerOnUnwind();
17691771
}
17701772

17711773
function handleThrow(root: FiberRoot, thrownValue: any): void {
@@ -2423,14 +2425,14 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
24232425
break;
24242426
}
24252427
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.
24342436
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
24352437
unitOfWork = workInProgress = resetWorkInProgress(
24362438
unitOfWork,

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

+200-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let use;
88
let useDebugValue;
99
let useState;
1010
let useMemo;
11+
let useEffect;
1112
let Suspense;
1213
let startTransition;
1314
let cache;
@@ -17,7 +18,7 @@ let waitForPaint;
1718
let assertLog;
1819
let waitForAll;
1920

20-
describe('ReactThenable', () => {
21+
describe('ReactUse', () => {
2122
beforeEach(() => {
2223
jest.resetModules();
2324

@@ -29,6 +30,7 @@ describe('ReactThenable', () => {
2930
useDebugValue = React.useDebugValue;
3031
useState = React.useState;
3132
useMemo = React.useMemo;
33+
useEffect = React.useEffect;
3234
Suspense = React.Suspense;
3335
startTransition = React.startTransition;
3436
cache = React.cache;
@@ -1182,4 +1184,201 @@ describe('ReactThenable', () => {
11821184
assertLog(['A1']);
11831185
expect(root).toMatchRenderedOutput('A1');
11841186
});
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+
});
11851384
});

‎packages/react-server/src/ReactFizzHooks.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -584,15 +584,7 @@ function use<T>(usable: Usable<T>): T {
584584
if (typeof usable.then === 'function') {
585585
// This is a thenable.
586586
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);
596588
} else if (
597589
usable.$$typeof === REACT_CONTEXT_TYPE ||
598590
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
@@ -606,6 +598,15 @@ function use<T>(usable: Usable<T>): T {
606598
throw new Error('An unsupported type was passed to use(): ' + String(usable));
607599
}
608600

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+
609610
function unsupportedRefresh() {
610611
throw new Error('Cache cannot be refreshed during server rendering.');
611612
}

‎packages/react-server/src/ReactFizzServer.js

+30
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ReactProviderType,
1919
OffscreenMode,
2020
Wakeable,
21+
Thenable,
2122
} from 'shared/ReactTypes';
2223
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
2324
import type {
@@ -102,6 +103,7 @@ import {
102103
currentResponseState,
103104
setCurrentResponseState,
104105
getThenableStateAfterSuspending,
106+
unwrapThenable,
105107
} from './ReactFizzHooks';
106108
import {DefaultCacheDispatcher} from './ReactFizzCache';
107109
import {getStackByComponentStackNode} from './ReactFizzComponentStack';
@@ -123,6 +125,7 @@ import {
123125
REACT_MEMO_TYPE,
124126
REACT_PROVIDER_TYPE,
125127
REACT_CONTEXT_TYPE,
128+
REACT_SERVER_CONTEXT_TYPE,
126129
REACT_SCOPE_TYPE,
127130
REACT_OFFSCREEN_TYPE,
128131
} from 'shared/ReactSymbols';
@@ -1440,6 +1443,33 @@ function renderNodeDestructiveImpl(
14401443
}
14411444
}
14421445

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+
14431473
// $FlowFixMe[method-unbinding]
14441474
const childString = Object.prototype.toString.call(node);
14451475

0 commit comments

Comments
 (0)
Please sign in to comment.