Repository: facebook/react
Pull request: [DevTools] Fix a crash when rendering a new class Component when simulating errored state



## Summary

The Fiber reconciler assumes that only committed Error boundaries could have been placed in `forceErrorForFibers`. However, DevTools let any unknown Fiber be considered a boundary for which we previously simulated an error. Not only does that incorrectly trigger updates resetting the state, it also causes a crash if we're rendering a Class Component for the first time as a result of simulating an errored state.

Now we stop considering all class Components as previously being an error boundary in fallback state. I left a comment in the Fiber reconciler why it's safe to assume that the instance is non-nullable when we're in the `shouldError(fiber) === false` path.


## How did you test this change?

- added test that reprod the crash


Changed files:
filename: packages/react-devtools-shared/src/__tests__/storeForceError-test.js
patch: @@ -0,0 +1,106 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type Store from 'react-devtools-shared/src/devtools/store';
+
+import {getVersionedRenderImplementation} from './utils';
+
+describe('Store forcing errors', () => {
+  let React;
+  let agent;
+  let store: Store;
+  let utils;
+  let actAsync;
+
+  beforeEach(() => {
+    agent = global.agent;
+    store = global.store;
+    store.collapseNodesByDefault = false;
+    store.componentFilters = [];
+    store.recordChangeDescriptions = true;
+
+    React = require('react');
+    utils = require('./utils');
+
+    actAsync = utils.actAsync;
+  });
+
+  const {render} = getVersionedRenderImplementation();
+
+  // @reactVersion >= 18.0
+  it('resets forced error and fallback states when filters are changed', async () => {
+    class AnyClassComponent extends React.Component {
+      render() {
+        return this.props.children;
+      }
+    }
+
+    class ErrorBoundary extends React.Component {
+      state = {hasError: false};
+
+      static getDerivedStateFromError() {
+        return {hasError: true};
+      }
+
+      render() {
+        if (this.state.hasError) {
+          return (
+            <AnyClassComponent key="fallback">
+              <div key="did-error" />
+            </AnyClassComponent>
+          );
+        }
+        return this.props.children;
+      }
+    }
+
+    function App() {
+      return (
+        <ErrorBoundary key="content">
+          <div key="error-content" />
+        </ErrorBoundary>
+      );
+    }
+
+    await actAsync(async () => {
+      render(<App />);
+    });
+    const rendererID = utils.getRendererID();
+    await actAsync(() => {
+      agent.overrideError({
+        id: store.getElementIDAtIndex(2),
+        rendererID,
+        forceError: true,
+      });
+    });
+
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <App>
+          ▾ <ErrorBoundary key="content">
+            ▾ <AnyClassComponent key="fallback">
+                <div key="did-error">
+    `);
+
+    await actAsync(() => {
+      agent.overrideError({
+        id: store.getElementIDAtIndex(2),
+        rendererID,
+        forceError: false,
+      });
+    });
+
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <App>
+          ▾ <ErrorBoundary key="content">
+              <div key="error-content">
+    `);
+  });
+});

filename: packages/react-devtools-shared/src/backend/fiber/renderer.js
patch: @@ -7898,7 +7898,7 @@ export function attach(
   // Map of Fiber and its force error status: true (error), false (toggled off)
   const forceErrorForFibers = new Map<Fiber, boolean>();
 
-  function shouldErrorFiberAccordingToMap(fiber: any): boolean {
+  function shouldErrorFiberAccordingToMap(fiber: any): boolean | null {
     if (typeof setErrorHandler !== 'function') {
       throw new Error(
         'Expected overrideError() to not get called for earlier React versions.',
@@ -7934,7 +7934,7 @@ export function attach(
       }
     }
     if (status === undefined) {
-      return false;
+      return null;
     }
     return status;
   }

filename: packages/react-reconciler/src/ReactFiberBeginWork.js
patch: @@ -1584,6 +1584,9 @@ function updateClassComponent(
     // This is used by DevTools to force a boundary to error.
     switch (shouldError(workInProgress)) {
       case false: {
+        // We previously simulated an error on this boundary
+        // so the instance must have been constructed in a previous
+        // commit.
         const instance = workInProgress.stateNode;
         const ctor = workInProgress.type;
         // TODO This way of resetting the error boundary state is a hack.