[DevTools] Fix a crash when rendering a new class Component when simulating errored state (#35985)
Sebastian "Sebbie" Silbermann committed
Mar 10, 2026 at 18:05 UTC
014138df87869b174956f90a33fd6cf66e160114
3 files changed
+111
-2
packages/react-devtools-shared/src/__tests__/storeForceError-test.js
new
+106
@@ -0,0 +1,106 @@
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
+import type Store from 'react-devtools-shared/src/devtools/store';
11
+
12
+import {getVersionedRenderImplementation} from './utils';
13
+
14
+describe('Store forcing errors', () => {
15
+ let React;
16
+ let agent;
17
+ let store: Store;
18
+ let utils;
19
+ let actAsync;
20
+
21
+ beforeEach(() => {
22
+ agent = global.agent;
23
+ store = global.store;
24
+ store.collapseNodesByDefault = false;
25
+ store.componentFilters = [];
26
+ store.recordChangeDescriptions = true;
27
+
28
+ React = require('react');
29
+ utils = require('./utils');
30
+
31
+ actAsync = utils.actAsync;
32
+ });
33
+
34
+ const {render} = getVersionedRenderImplementation();
35
+
36
+ // @reactVersion >= 18.0
37
+ it('resets forced error and fallback states when filters are changed', async () => {
38
+ class AnyClassComponent extends React.Component {
39
+ render() {
40
+ return this.props.children;
41
+ }
42
+ }
43
+
44
+ class ErrorBoundary extends React.Component {
45
+ state = {hasError: false};
46
+
47
+ static getDerivedStateFromError() {
48
+ return {hasError: true};
49
+ }
50
+
51
+ render() {
52
+ if (this.state.hasError) {
53
+ return (
54
+ <AnyClassComponent key="fallback">
55
+ <div key="did-error" />
56
+ </AnyClassComponent>
57
+ );
58
+ }
59
+ return this.props.children;
60
+ }
61
+ }
62
+
63
+ function App() {
64
+ return (
65
+ <ErrorBoundary key="content">
66
+ <div key="error-content" />
67
+ </ErrorBoundary>
68
+ );
69
+ }
70
+
71
+ await actAsync(async () => {
72
+ render(<App />);
73
+ });
74
+ const rendererID = utils.getRendererID();
75
+ await actAsync(() => {
76
+ agent.overrideError({
77
+ id: store.getElementIDAtIndex(2),
78
+ rendererID,
79
+ forceError: true,
80
+ });
81
+ });
82
+
83
+ expect(store).toMatchInlineSnapshot(`
84
+ [root]
85
+ ▾ <App>
86
+ ▾ <ErrorBoundary key="content">
87
+ ▾ <AnyClassComponent key="fallback">
88
+ <div key="did-error">
89
+ `);
90
+
91
+ await actAsync(() => {
92
+ agent.overrideError({
93
+ id: store.getElementIDAtIndex(2),
94
+ rendererID,
95
+ forceError: false,
96
+ });
97
+ });
98
+
99
+ expect(store).toMatchInlineSnapshot(`
100
+ [root]
101
+ ▾ <App>
102
+ ▾ <ErrorBoundary key="content">
103
+ <div key="error-content">
104
+ `);
105
+ });
106
+});
packages/react-devtools-shared/src/backend/fiber/renderer.js
+2
-2
@@ -7898,7 +7898,7 @@ export function attach(
7898
// Map of Fiber and its force error status: true (error), false (toggled off)
7899
const forceErrorForFibers = new Map<Fiber, boolean>();
7900
7901
- function shouldErrorFiberAccordingToMap(fiber: any): boolean {
7901
+ function shouldErrorFiberAccordingToMap(fiber: any): boolean | null {
7902
if (typeof setErrorHandler !== 'function') {
7903
throw new Error(
7904
'Expected overrideError() to not get called for earlier React versions.',
@@ -7934,7 +7934,7 @@ export function attach(
7934
}
7935
}
7936
if (status === undefined) {
7937
- return false;
7937
+ return null;
7938
}
7939
return status;
7940
}
packages/react-reconciler/src/ReactFiberBeginWork.js
+3
@@ -1584,6 +1584,9 @@ function updateClassComponent(
1584
// This is used by DevTools to force a boundary to error.
1585
switch (shouldError(workInProgress)) {
1586
case false: {
1587
+ // We previously simulated an error on this boundary
1588
+ // so the instance must have been constructed in a previous
1589
+ // commit.
1590
const instance = workInProgress.stateNode;
1591
const ctor = workInProgress.type;
1592
// TODO This way of resetting the error boundary state is a hack.