Repository: facebook/react
Pull request: [DevTools] Fix memory leak when unmounting hoistables

## Summary

For Host Hoistables we have may have many devtools instances to choose from as the nearest instance since React Float may have deduplicated the public host instance. We keep an internal map from the public host instance to the chosen devtools instance. When we were unmounting a Host Hoistable, we pick the next Fiber that's also referencing the same public host instance.

Unfortunately we were using the wrong values when moving those pointers. This wasn't caught by Flow since our `HostInstance` type is any object so you can assign anything to it and assign it to anything (here we assigned a `DevToolsInstance` to a `HostInstance`).

We could improve type safety for those operations but it's quite involved. It's the only bug where we currently accidentally assign these two types. If this happens more often, we can harden the types.

Also ensures element inspection works when unmounting Hoist Hoistables. You'd have to actually inspect the DOM node to notice the bug. The memory leak you always get so I choose that bug as the headline.

Found with Claude + Opus 4.6

## How did you test this change?

- [x] Added test (1st commit has previous behavior)

Changed files:
filename: packages/react-devtools-shared/src/__tests__/store-test.js
patch: @@ -3616,4 +3616,59 @@ describe('Store', () => {
               <div>
     `);
   });
+
+  // @reactVersion >= 19
+  it('cleans up host hoistables', async () => {
+    function Left() {
+      return (
+        <style href="test.css" precedence="medium">
+          {'* {color:black}'}
+        </style>
+      );
+    }
+
+    function Right() {
+      return (
+        <style href="test.css" precedence="medium">
+          {'* {color:black}'}
+        </style>
+      );
+    }
+
+    await actAsync(() => {
+      render(
+        <>
+          <Left />
+          <Right />
+        </>,
+      );
+    });
+
+    // Ensure we're still testing deduplicated hoistables.
+    expect(document.head.querySelectorAll('style')).toHaveLength(1);
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+          <Left>
+          <Right>
+    `);
+    let style = document.head.querySelector('style');
+    let styleID = agent.getIDForHostInstance(style).id;
+    expect(store.containsElement(styleID)).toBe(true);
+
+    await actAsync(() => {
+      render(
+        <>
+          <Right />
+        </>,
+      );
+    });
+
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+          <Right>
+    `);
+    style = document.head.querySelector('style');
+    styleID = agent.getIDForHostInstance(style).id;
+    expect(store.containsElement(styleID)).toBe(true);
+  });
 });

filename: packages/react-devtools-shared/src/backend/fiber/renderer.js
patch: @@ -991,8 +991,8 @@ function releaseHostResource(
         // eslint-disable-next-line no-for-of-loops/no-for-of-loops
         for (const firstInstance of resourceInstances) {
           publicInstanceToDevToolsInstanceMap.set(
+            publicInstance,
             firstInstance,
-            nearestInstance,
           );
           break;
         }