7
* @flow
8
*/
9
10
-import type {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes';
10
+import type {
11
+ ReactComponentInfo,
12
+ ReactDebugInfo,
13
+ ReactAsyncInfo,
14
+} from 'shared/ReactTypes';
15
16
import {
17
ComponentFilterDisplayName,
139
ReactRenderer,
140
RendererInterface,
141
SerializedElement,
142
+ SerializedAsyncInfo,
143
WorkTagMap,
144
CurrentDispatcherRef,
145
LegacyDispatcherRef,
170
source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack
171
logCount: number, // total number of errors/warnings last seen
172
treeBaseDuration: number, // the profiled time of the last render of this subtree
173
+ suspendedBy: null | Array<ReactAsyncInfo>, // things that suspended in the children position of this component
174
data: Fiber, // one of a Fiber pair
175
};
176
184
source: null,
185
logCount: 0,
186
treeBaseDuration: 0,
187
+ suspendedBy: null,
188
data: fiber,
189
};
190
}
200
source: null | string | Error | ReactFunctionLocation, // always null here.
201
logCount: number, // total number of errors/warnings last seen
202
treeBaseDuration: number, // the profiled time of the last render of this subtree
203
+ suspendedBy: null | Array<ReactAsyncInfo>, // not used
204
data: Fiber, // one of a Fiber pair
205
};
206
215
source: null,
216
logCount: 0,
217
treeBaseDuration: 0,
218
+ suspendedBy: null,
219
data: fiber,
220
}: any);
221
}
234
source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack
235
logCount: number, // total number of errors/warnings last seen
236
treeBaseDuration: number, // the profiled time of the last render of this subtree
237
+ suspendedBy: null | Array<ReactAsyncInfo>, // things that blocked the server component's child from rendering
238
// The latest info for this instance. This can be updated over time and the
239
// same info can appear in more than once ServerComponentInstance.
240
data: ReactComponentInfo,
252
source: null,
253
logCount: 0,
254
treeBaseDuration: 0,
255
+ suspendedBy: null,
256
data: debugEntry,
257
};
258
}
2365
// the current parent here as well.
2366
let reconcilingParent: null | DevToolsInstance = null;
2367
2368
+ function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void {
2369
+ const parentInstance = reconcilingParent;
2370
+ if (parentInstance === null) {
2371
+ // Suspending at the root is not attributed to any particular component
2372
+ // TODO: It should be attributed to the shell.
2373
+ return;
2374
+ }
2375
+ const suspendedBy = parentInstance.suspendedBy;
2376
+ if (suspendedBy === null) {
2377
+ parentInstance.suspendedBy = [asyncInfo];
2378
+ } else if (suspendedBy.indexOf(asyncInfo) === -1) {
2379
+ suspendedBy.push(asyncInfo);
2380
+ }
2381
+ }
2382
+
2383
function insertChild(instance: DevToolsInstance): void {
2384
const parentInstance = reconcilingParent;
2385
if (parentInstance === null) {
2541
if (fiber._debugInfo) {
2542
for (let i = 0; i < fiber._debugInfo.length; i++) {
2543
const debugEntry = fiber._debugInfo[i];
2544
+ if (debugEntry.awaited) {
2545
+ // Async Info
2546
+ const asyncInfo: ReactAsyncInfo = (debugEntry: any);
2547
+ if (level === virtualLevel) {
2548
+ // Track any async info between the previous virtual instance up until to this
2549
+ // instance and add it to the parent. This can add the same set multiple times
2550
+ // so we assume insertSuspendedBy dedupes.
2551
+ insertSuspendedBy(asyncInfo);
2552
+ }
2553
+ if (previousVirtualInstance) continue;
2554
+ }
2555
if (typeof debugEntry.name !== 'string') {
2556
// Not a Component. Some other Debug Info.
2557
continue;
2805
// Move all the children of this instance to the remaining set.
2806
remainingReconcilingChildren = instance.firstChild;
2807
instance.firstChild = null;
2808
+ instance.suspendedBy = null;
2809
try {
2810
// Unmount the remaining set.
2811
unmountRemainingChildren();
3006
// We'll move them back one by one, and anything that remains is deleted.
3007
remainingReconcilingChildren = virtualInstance.firstChild;
3008
virtualInstance.firstChild = null;
3009
+ virtualInstance.suspendedBy = null;
3010
try {
3011
if (
3012
updateVirtualChildrenRecursively(
3058
if (nextChild._debugInfo) {
3059
for (let i = 0; i < nextChild._debugInfo.length; i++) {
3060
const debugEntry = nextChild._debugInfo[i];
3061
+ if (debugEntry.awaited) {
3062
+ // Async Info
3063
+ const asyncInfo: ReactAsyncInfo = (debugEntry: any);
3064
+ if (level === virtualLevel) {
3065
+ // Track any async info between the previous virtual instance up until to this
3066
+ // instance and add it to the parent. This can add the same set multiple times
3067
+ // so we assume insertSuspendedBy dedupes.
3068
+ insertSuspendedBy(asyncInfo);
3069
+ }
3070
+ if (previousVirtualInstance) continue;
3071
+ }
3072
if (typeof debugEntry.name !== 'string') {
3073
// Not a Component. Some other Debug Info.
3074
continue;
3393
// We'll move them back one by one, and anything that remains is deleted.
3394
remainingReconcilingChildren = fiberInstance.firstChild;
3395
fiberInstance.firstChild = null;
3396
+ fiberInstance.suspendedBy = null;
3397
}
3398
try {
3399
if (
4102
return null;
4103
}
4104
4105
+ function serializeAsyncInfo(
4106
+ asyncInfo: ReactAsyncInfo,
4107
+ index: number,
4108
+ parentInstance: DevToolsInstance,
4109
+ ): SerializedAsyncInfo {
4110
+ const ioInfo = asyncInfo.awaited;
4111
+ const ioOwnerInstance = findNearestOwnerInstance(
4112
+ parentInstance,
4113
+ ioInfo.owner,
4114
+ );
4115
+ const awaitOwnerInstance = findNearestOwnerInstance(
4116
+ parentInstance,
4117
+ asyncInfo.owner,
4118
+ );
4119
+ return {
4120
+ awaited: {
4121
+ name: ioInfo.name,
4122
+ start: ioInfo.start,
4123
+ end: ioInfo.end,
4124
+ value: ioInfo.value == null ? null : ioInfo.value,
4125
+ env: ioInfo.env == null ? null : ioInfo.env,
4126
+ owner:
4127
+ ioOwnerInstance === null
4128
+ ? null
4129
+ : instanceToSerializedElement(ioOwnerInstance),
4130
+ stack: ioInfo.stack == null ? null : ioInfo.stack,
4131
+ },
4132
+ env: asyncInfo.env == null ? null : asyncInfo.env,
4133
+ owner:
4134
+ awaitOwnerInstance === null
4135
+ ? null
4136
+ : instanceToSerializedElement(awaitOwnerInstance),
4137
+ stack: asyncInfo.stack == null ? null : asyncInfo.stack,
4138
+ };
4139
+ }
4140
+
4141
// Fast path props lookup for React Native style editor.
4142
// Could use inspectElementRaw() but that would require shallow rendering hooks components,
4143
// and could also mess with memoization.
4429
nativeTag = getNativeTag(fiber.stateNode);
4430
}
4431
4432
+ // This set is an edge case where if you pass a promise to a Client Component into a children
4433
+ // position without a Server Component as the direct parent. E.g. <div>{promise}</div>
4434
+ // In this case, this becomes associated with the Client/Host Component where as normally
4435
+ // you'd expect these to be associated with the Server Component that awaited the data.
4436
+ // TODO: Prepend other suspense sources like css, images and use().
4437
+ const suspendedBy = fiberInstance.suspendedBy;
4438
+
4439
return {
4440
id: fiberInstance.id,
4441
4492
? []
4493
: Array.from(componentLogsEntry.warnings.entries()),
4494
4495
+ suspendedBy:
4496
+ suspendedBy === null
4497
+ ? []
4498
+ : suspendedBy.map((info, index) =>
4499
+ serializeAsyncInfo(info, index, fiberInstance),
4500
+ ),
4501
+
4502
// List of owners
4503
owners,
4504
4552
const componentLogsEntry =
4553
componentInfoToComponentLogsMap.get(componentInfo);
4554
4555
+ // Things that Suspended this Server Component (use(), awaits and direct child promises)
4556
+ const suspendedBy = virtualInstance.suspendedBy;
4557
+
4558
return {
4559
id: virtualInstance.id,
4560
4594
componentLogsEntry === undefined
4595
? []
4596
: Array.from(componentLogsEntry.warnings.entries()),
4597
+
4598
+ suspendedBy:
4599
+ suspendedBy === null
4600
+ ? []
4601
+ : suspendedBy.map((info, index) =>
4602
+ serializeAsyncInfo(info, index, virtualInstance),
4603
+ ),
4604
+
4605
// List of owners
4606
owners,
4607
4646
4647
function createIsPathAllowed(
4648
key: string | null,
4537
- secondaryCategory: 'hooks' | null,
4649
+ secondaryCategory: 'suspendedBy' | 'hooks' | null,
4650
) {
4651
// This function helps prevent previously-inspected paths from being dehydrated in updates.
4652
// This is important to avoid a bad user experience where expanded toggles collapse on update.
4678
return true;
4679
}
4680
break;
4681
+ case 'suspendedBy':
4682
+ if (path.length < 5) {
4683
+ // Never dehydrate anything above suspendedBy[index].awaited.value
4684
+ // Those are part of the internal meta data. We only dehydrate inside the Promise.
4685
+ return true;
4686
+ }
4687
+ break;
4688
default:
4689
break;
4690
}
4908
type: 'not-found',
4909
};
4910
}
4911
+ const inspectedElement = mostRecentlyInspectedElement;
4912
4913
// Any time an inspected element has an update,
4914
// we should update the selected $r value as wel.
4915
// Do this before dehydration (cleanForBridge).
4796
- updateSelectedElement(mostRecentlyInspectedElement);
4916
+ updateSelectedElement(inspectedElement);
4917
4918
// Clone before cleaning so that we preserve the full data.
4919
// This will enable us to send patches without re-inspecting if hydrated paths are requested.
4920
// (Reducing how often we shallow-render is a better DX for function components that use hooks.)
4801
- const cleanedInspectedElement = {...mostRecentlyInspectedElement};
4921
+ const cleanedInspectedElement = {...inspectedElement};
4922
// $FlowFixMe[prop-missing] found when upgrading Flow
4923
cleanedInspectedElement.context = cleanForBridge(
4804
- cleanedInspectedElement.context,
4924
+ inspectedElement.context,
4925
createIsPathAllowed('context', null),
4926
);
4927
// $FlowFixMe[prop-missing] found when upgrading Flow
4928
cleanedInspectedElement.hooks = cleanForBridge(
4809
- cleanedInspectedElement.hooks,
4929
+ inspectedElement.hooks,
4930
createIsPathAllowed('hooks', 'hooks'),
4931
);
4932
// $FlowFixMe[prop-missing] found when upgrading Flow
4933
cleanedInspectedElement.props = cleanForBridge(
4814
- cleanedInspectedElement.props,
4934
+ inspectedElement.props,
4935
createIsPathAllowed('props', null),
4936
);
4937
// $FlowFixMe[prop-missing] found when upgrading Flow
4938
cleanedInspectedElement.state = cleanForBridge(
4819
- cleanedInspectedElement.state,
4939
+ inspectedElement.state,
4940
createIsPathAllowed('state', null),
4941
);
4942
+ // $FlowFixMe[prop-missing] found when upgrading Flow
4943
+ cleanedInspectedElement.suspendedBy = cleanForBridge(
4944
+ inspectedElement.suspendedBy,
4945
+ createIsPathAllowed('suspendedBy', 'suspendedBy'),
4946
+ );
4947
4948
return {
4949
id,