[DevTools] Fix broken commit tree builder for initial operations (#35710)
Sebastian "Sebbie" Silbermann committed
Feb 6, 2026 at 15:16 UTC
2a879cdc95228b1b3b4cdc81cfc04599716b5562
5 files changed
+28
-66
packages/react-devtools-shared/src/backend/fiber/renderer.js
+27
-14
@@ -1138,7 +1138,7 @@ export function attach(
1138
// if any passive effects called console.warn / console.error.
1139
let needsToFlushComponentLogs = false;
1140
1141
- function bruteForceFlushErrorsAndWarnings() {
1141
+ function bruteForceFlushErrorsAndWarnings(root: FiberInstance) {
1142
// Refresh error/warning count for all mounted unfiltered Fibers.
1143
let hasChanges = false;
1144
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
@@ -1156,7 +1156,7 @@ export function attach(
1156
}
1157
}
1158
if (hasChanges) {
1159
- flushPendingEvents();
1159
+ flushPendingEvents(root);
1160
}
1161
}
1162
@@ -1183,7 +1183,7 @@ export function attach(
1183
updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1184
}
1185
}
1186
- flushPendingEvents();
1186
+ flushPendingEvents(null);
1187
}
1188
1189
function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {
@@ -1211,7 +1211,7 @@ export function attach(
1211
}
1212
const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
1213
if (changed) {
1214
- flushPendingEvents();
1214
+ flushPendingEvents(null);
1215
updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1216
}
1217
}
@@ -1533,6 +1533,8 @@ export function attach(
1533
if (isProfiling) {
1534
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
1535
// If necessary, we could support this- but it doesn't seem like a necessary use case.
1536
+ // Supporting change of filters while profiling would require a refactor
1537
+ // to flush after each root instead of at the end.
1538
throw Error('Cannot modify filter preferences while profiling');
1539
}
1540
@@ -1647,7 +1649,8 @@ export function attach(
1649
focusedActivityFilter.activityID = focusedActivityID;
1650
}
1651
1650
- flushPendingEvents();
1652
+ // We're not profiling so it's safe to flush without a specific root.
1653
+ flushPendingEvents(null);
1654
1655
needsToFlushComponentLogs = false;
1656
}
@@ -2203,7 +2206,12 @@ export function attach(
2206
}
2207
}
2208
2206
- function flushPendingEvents(): void {
2209
+ /**
2210
+ * Allowed to flush pending events without a specific root when:
2211
+ * - pending operations don't record tree mutations e.g. TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS
2212
+ * - not profiling (the commit tree builder requires the root of the mutations)
2213
+ */
2214
+ function flushPendingEvents(root: FiberInstance | null): void {
2215
if (shouldBailoutWithPendingOperations()) {
2216
// If we aren't profiling, we can just bail out here.
2217
// No use sending an empty update over the bridge.
@@ -2245,11 +2253,10 @@ export function attach(
2253
// Which in turn enables fiber props, states, and hooks to be inspected.
2254
let i = 0;
2255
operations[i++] = rendererID;
2248
- if (currentRoot === null) {
2249
- // TODO: This is not always safe so this field is probably not needed.
2256
+ if (root === null) {
2257
operations[i++] = -1;
2258
} else {
2252
- operations[i++] = currentRoot.id;
2259
+ operations[i++] = root.id;
2260
}
2261
2262
// Now fill in the string table.
@@ -5746,11 +5753,11 @@ export function attach(
5753
5754
mountFiberRecursively(root.current, false);
5755
5756
+ flushPendingEvents(currentRoot);
5757
+
5758
currentRoot = (null: any);
5759
});
5760
5752
- flushPendingEvents();
5753
-
5761
needsToFlushComponentLogs = false;
5762
}
5763
}
@@ -5760,7 +5767,7 @@ export function attach(
5767
// safe to stop calling it from Fiber.
5768
}
5769
5763
- function handlePostCommitFiberRoot(root: any) {
5770
+ function handlePostCommitFiberRoot(root: FiberRoot) {
5771
if (isProfiling && rootSupportsProfiling(root)) {
5772
if (currentCommitProfilingMetadata !== null) {
5773
const {effectDuration, passiveEffectDuration} =
@@ -5774,12 +5781,18 @@ export function attach(
5781
}
5782
5783
if (needsToFlushComponentLogs) {
5784
+ const rootInstance = rootToFiberInstanceMap.get(root);
5785
+ if (rootInstance === undefined) {
5786
+ throw new Error(
5787
+ 'Should have a root instance for a committed root. This is a bug in React DevTools.',
5788
+ );
5789
+ }
5790
// We received new logs after commit. I.e. in a passive effect. We need to
5791
// traverse the tree to find the affected ones. If we just moved the whole
5792
// tree traversal from handleCommitFiberRoot to handlePostCommitFiberRoot
5793
// this wouldn't be needed. For now we just brute force check all instances.
5794
// This is not that common of a case.
5782
- bruteForceFlushErrorsAndWarnings();
5795
+ bruteForceFlushErrorsAndWarnings(rootInstance);
5796
}
5797
}
5798
@@ -5876,7 +5889,7 @@ export function attach(
5889
}
5890
5891
// We're done here.
5879
- flushPendingEvents();
5892
+ flushPendingEvents(currentRoot);
5893
5894
needsToFlushComponentLogs = false;
5895
packages/react-devtools-shared/src/constants.js
+1
-1
@@ -22,7 +22,7 @@ export const TREE_OPERATION_REMOVE = 2;
22
export const TREE_OPERATION_REORDER_CHILDREN = 3;
23
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
24
export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
25
-export const TREE_OPERATION_REMOVE_ROOT = 6;
25
+// Removed `TREE_OPERATION_REMOVE_ROOT`
26
export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
27
export const SUSPENSE_TREE_OPERATION_ADD = 8;
28
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
packages/react-devtools-shared/src/devtools/store.js
-40
@@ -16,7 +16,6 @@ import {
16
PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
17
TREE_OPERATION_ADD,
18
TREE_OPERATION_REMOVE,
19
- TREE_OPERATION_REMOVE_ROOT,
19
TREE_OPERATION_REORDER_CHILDREN,
20
TREE_OPERATION_SET_SUBTREE_MODE,
21
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
@@ -1644,45 +1643,6 @@ export default class Store extends EventEmitter<{
1643
1644
break;
1645
}
1647
- case TREE_OPERATION_REMOVE_ROOT: {
1648
- i += 1;
1649
-
1650
- const id = operations[1];
1651
-
1652
- if (__DEBUG__) {
1653
- debug(`Remove root ${id}`);
1654
- }
1655
-
1656
- const recursivelyDeleteElements = (elementID: number) => {
1657
- const element = this._idToElement.get(elementID);
1658
- this._idToElement.delete(elementID);
1659
- if (element) {
1660
- // Mostly for Flow's sake
1661
- for (let index = 0; index < element.children.length; index++) {
1662
- recursivelyDeleteElements(element.children[index]);
1663
- }
1664
- }
1665
- };
1666
-
1667
- const root = this._idToElement.get(id);
1668
- if (root === undefined) {
1669
- this._throwAndEmitError(
1670
- Error(
1671
- `Cannot remove root "${id}": no matching node was found in the Store.`,
1672
- ),
1673
- );
1674
-
1675
- break;
1676
- }
1677
-
1678
- recursivelyDeleteElements(id);
1679
-
1680
- this._rootIDToCapabilities.delete(id);
1681
- this._rootIDToRendererID.delete(id);
1682
- this._roots = this._roots.filter(rootID => rootID !== id);
1683
- this._weightAcrossRoots -= root.weight;
1684
- break;
1685
- }
1646
case TREE_OPERATION_REORDER_CHILDREN: {
1647
const id = operations[i + 1];
1648
const numChildren = operations[i + 2];
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
-4
@@ -11,7 +11,6 @@ import {
11
__DEBUG__,
12
TREE_OPERATION_ADD,
13
TREE_OPERATION_REMOVE,
14
- TREE_OPERATION_REMOVE_ROOT,
14
TREE_OPERATION_REORDER_CHILDREN,
15
TREE_OPERATION_SET_SUBTREE_MODE,
16
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
@@ -313,9 +312,6 @@ function updateTree(
312
}
313
break;
314
}
316
- case TREE_OPERATION_REMOVE_ROOT: {
317
- throw Error('Operation REMOVE_ROOT is not supported while profiling.');
318
- }
315
case TREE_OPERATION_REORDER_CHILDREN: {
316
id = ((operations[i + 1]: any): number);
317
const numChildren = ((operations[i + 2]: any): number);
packages/react-devtools-shared/src/utils.js
-7
@@ -28,7 +28,6 @@ import {
28
import {
29
TREE_OPERATION_ADD,
30
TREE_OPERATION_REMOVE,
31
- TREE_OPERATION_REMOVE_ROOT,
31
TREE_OPERATION_REORDER_CHILDREN,
32
TREE_OPERATION_SET_SUBTREE_MODE,
33
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
@@ -295,12 +294,6 @@ export function printOperationsArray(operations: Array<number>) {
294
}
295
break;
296
}
298
- case TREE_OPERATION_REMOVE_ROOT: {
299
- i += 1;
300
-
301
- logs.push(`Remove root ${rootID}`);
302
- break;
303
- }
297
case TREE_OPERATION_SET_SUBTREE_MODE: {
298
const id = operations[i + 1];
299
const mode = operations[i + 2];