Skip to content

Commit fa3cf50

Browse files
authored
[DevTools] Add Map for Server Component Logs (#30905)
Stacked on #30899. This adds another map to store Server Components logs. When they're replayed with an owner we can associate them with a DevToolsInstance. The replaying should happen before they can mount in Fiber so they'll always have all logs when they mount. There can be more than one Instance associated with any particular ReactComponentInfo. It can also be unmounted and restored later. One thing that's interesting about these is that when a Server Component tree refreshes a new set of ReactComponentInfo will update through the tree and the VirtualInstances will update with new instances. This means that the old errors/warnings are no longer associated with the VirtualInstance. I.e. it's not continually appended like updates do for Fiber backed instances. On the client we dedupe errors/warnings for the life time of the page. On the server that doesn't work well because it would mean that when you refresh the page, you miss out on warnings so we dedupe them per request instead. If we just appended on refresh it would keep adding them. If ever add a deduping mechanism that spans longer than a request, we might need to do more of a merge when these updates. Nothing actually adds logs to this map yet. That will need an integration with Flight in a follow up.
1 parent f4b3a1f commit fa3cf50

File tree

2 files changed

+76
-23
lines changed

2 files changed

+76
-23
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

+47-23
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ import {
100100
SERVER_CONTEXT_SYMBOL_STRING,
101101
} from '../shared/ReactSymbols';
102102
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
103+
104+
import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';
105+
103106
import is from 'shared/objectIs';
104107
import hasOwnProperty from 'shared/hasOwnProperty';
105108

@@ -995,7 +998,8 @@ export function attach(
995998
// Note, this only clears logs for Fibers that have instances. If they're filtered
996999
// and then mount, the logs are there. Ensuring we only clear what you've seen.
9971000
// If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a
998-
// new WeakMap.
1001+
// new WeakMap. It's unclear whether we should clear componentInfoToComponentLogsMap
1002+
// since it's shared by other renderers but presumably it would.
9991003

10001004
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
10011005
for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
@@ -1006,7 +1010,7 @@ export function attach(
10061010
fiberToComponentLogsMap.delete(fiber.alternate);
10071011
}
10081012
} else {
1009-
// TODO: Handle VirtualInstance.
1013+
componentInfoToComponentLogsMap.delete(devtoolsInstance.data);
10101014
}
10111015
const changed = recordConsoleLogs(devtoolsInstance, undefined);
10121016
if (changed) {
@@ -1019,28 +1023,27 @@ export function attach(
10191023
function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {
10201024
const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
10211025
if (devtoolsInstance !== undefined) {
1026+
let componentLogsEntry;
10221027
if (devtoolsInstance.kind === FIBER_INSTANCE) {
10231028
const fiber = devtoolsInstance.data;
1024-
const componentLogsEntry = fiberToComponentLogsMap.get(fiber);
1025-
if (componentLogsEntry !== undefined) {
1026-
if (type === 'error') {
1027-
componentLogsEntry.errors.clear();
1028-
componentLogsEntry.errorsCount = 0;
1029-
} else {
1030-
componentLogsEntry.warnings.clear();
1031-
componentLogsEntry.warningsCount = 0;
1032-
}
1033-
const changed = recordConsoleLogs(
1034-
devtoolsInstance,
1035-
componentLogsEntry,
1036-
);
1037-
if (changed) {
1038-
flushPendingEvents();
1039-
updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1040-
}
1041-
}
1029+
componentLogsEntry = fiberToComponentLogsMap.get(fiber);
10421030
} else {
1043-
// TODO: Handle VirtualInstance.
1031+
const componentInfo = devtoolsInstance.data;
1032+
componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);
1033+
}
1034+
if (componentLogsEntry !== undefined) {
1035+
if (type === 'error') {
1036+
componentLogsEntry.errors.clear();
1037+
componentLogsEntry.errorsCount = 0;
1038+
} else {
1039+
componentLogsEntry.warnings.clear();
1040+
componentLogsEntry.warningsCount = 0;
1041+
}
1042+
const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
1043+
if (changed) {
1044+
flushPendingEvents();
1045+
updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1046+
}
10441047
}
10451048
}
10461049
}
@@ -2188,6 +2191,10 @@ export function attach(
21882191
pushOperation(ownerID);
21892192
pushOperation(displayNameStringID);
21902193
pushOperation(keyStringID);
2194+
2195+
const componentLogsEntry =
2196+
componentInfoToComponentLogsMap.get(componentInfo);
2197+
recordConsoleLogs(instance, componentLogsEntry);
21912198
}
21922199

21932200
function recordUnmount(fiberInstance: FiberInstance): void {
@@ -2857,6 +2864,14 @@ export function attach(
28572864
) {
28582865
recordResetChildren(virtualInstance);
28592866
}
2867+
// Update the errors/warnings count. If this Instance has switched to a different
2868+
// ReactComponentInfo instance, such as when refreshing Server Components, then
2869+
// we replace all the previous logs with the ones associated with the new ones rather
2870+
// than merging. Because deduping is expected to happen at the request level.
2871+
const componentLogsEntry = componentInfoToComponentLogsMap.get(
2872+
virtualInstance.data,
2873+
);
2874+
recordConsoleLogs(virtualInstance, componentLogsEntry);
28602875
// Must be called after all children have been appended.
28612876
recordVirtualProfilingDurations(virtualInstance);
28622877
} finally {
@@ -4293,6 +4308,9 @@ export function attach(
42934308
stylex: null,
42944309
};
42954310

4311+
const componentLogsEntry =
4312+
componentInfoToComponentLogsMap.get(componentInfo);
4313+
42964314
return {
42974315
id: virtualInstance.id,
42984316

@@ -4326,8 +4344,14 @@ export function attach(
43264344
hooks: null,
43274345
props: props,
43284346
state: null,
4329-
errors: [], // TODO: Handle errors on Virtual Instances.
4330-
warnings: [], // TODO: Handle warnings on Virtual Instances.
4347+
errors:
4348+
componentLogsEntry === undefined
4349+
? []
4350+
: Array.from(componentLogsEntry.errors.entries()),
4351+
warnings:
4352+
componentLogsEntry === undefined
4353+
? []
4354+
: Array.from(componentLogsEntry.warnings.entries()),
43314355
// List of owners
43324356
owners,
43334357

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// This keeps track of Server Component logs which may come from.
11+
// This is in a shared module because Server Component logs don't come from a specific renderer
12+
// but can become associated with a Virtual Instance of any renderer.
13+
14+
import type {ReactComponentInfo} from 'shared/ReactTypes';
15+
16+
type ComponentLogs = {
17+
errors: Map<string, number>,
18+
errorsCount: number,
19+
warnings: Map<string, number>,
20+
warningsCount: number,
21+
};
22+
23+
// This keeps it around as long as the ComponentInfo is alive which
24+
// lets the Fiber get reparented/remounted and still observe the previous errors/warnings.
25+
// Unless we explicitly clear the logs from a Fiber.
26+
export const componentInfoToComponentLogsMap: WeakMap<
27+
ReactComponentInfo,
28+
ComponentLogs,
29+
> = new WeakMap();

0 commit comments

Comments
 (0)