Repository: facebook/react
Pull request: [DevTools] Fix null ref crash in ContextMenu when items list is empty

## Summary

Fixes #35923

When the `ContextMenu` component renders with an empty `items` list (or when the portal container is missing), the component returns `null` and the portal `<div ref={ref}>` is never mounted. However, the `useLayoutEffect` still fires and calls `repositionToFit(ref.current, ...)` where `ref.current` is `null`, causing:

```
TypeError: Cannot read properties of null (reading 'ownerDocument')
```

This happens in the Timeline profiler view when the user right-clicks on the canvas before hovering over any specific event, resulting in an empty context menu items list being passed to the `ContextMenu` component.

## How did you test this change?

Added `contextMenu-test.js` with two test cases:
1. Verifies the component does not crash when `items` is empty (reproduces the exact error from the issue)
2. Verifies the component renders correctly when items are provided

Steps to reproduce the original bug:
1. Open React DevTools
2. Record a profiling session
3. Switch to the Timeline tab
4. Right-click on the canvas area (not on a specific event)

With this fix, the right-click on an empty area no longer crashes.

Changed files:
filename: packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js
patch: @@ -8,15 +8,14 @@
  */
 
 import * as React from 'react';
-import {useLayoutEffect, createRef} from 'react';
+import {useLayoutEffect} from 'react';
 import {createPortal} from 'react-dom';
 
 import ContextMenuItem from './ContextMenuItem';
 
 import type {
   ContextMenuItem as ContextMenuItemType,
   ContextMenuPosition,
-  ContextMenuRef,
 } from './types';
 
 import styles from './ContextMenu.css';
@@ -49,15 +48,13 @@ type Props = {
   items: ContextMenuItemType[],
   position: ContextMenuPosition,
   hide: () => void,
-  ref?: ContextMenuRef,
 };
 
 export default function ContextMenu({
   anchorElementRef,
   position,
   items,
   hide,
-  ref = createRef(),
 }: Props): React.Node {
   // This works on the assumption that ContextMenu component is only rendered when it should be shown
   const anchor = anchorElementRef.current;
@@ -73,8 +70,21 @@ export default function ContextMenu({
     '[data-react-devtools-portal-root]',
   );
 
+  const hideMenu = portalContainer == null || items.length === 0;
+  const menuRef = React.useRef<HTMLDivElement | null>(null);
+
   useLayoutEffect(() => {
-    const menu = ((ref.current: any): HTMLElement);
+    // Match the early-return condition below.
+    if (hideMenu) {
+      return;
+    }
+    const maybeMenu = menuRef.current;
+    if (maybeMenu === null) {
+      throw new Error(
+        "Can't access context menu element. This is a bug in React DevTools.",
+      );
+    }
+    const menu = (maybeMenu: HTMLDivElement);
 
     function hideUnlessContains(event: Event) {
       if (!menu.contains(((event.target: any): Node))) {
@@ -98,14 +108,14 @@ export default function ContextMenu({
 
       ownerWindow.removeEventListener('resize', hide);
     };
-  }, []);
+  }, [hideMenu]);
 
-  if (portalContainer == null || items.length === 0) {
+  if (hideMenu) {
     return null;
   }
 
   return createPortal(
-    <div className={styles.ContextMenu} ref={ref}>
+    <div className={styles.ContextMenu} ref={menuRef}>
       {items.map(({onClick, content}, index) => (
         <ContextMenuItem key={index} onClick={onClick} hide={hide}>
           {content}

filename: packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer.js
patch: @@ -53,7 +53,6 @@ export default function ContextMenuContainer({
       position={position}
       hide={hide}
       items={items}
-      ref={ref}
     />
   );
 }