From 605949b34692ad0f9bbefcfee4b4bdde274396f0 Mon Sep 17 00:00:00 2001 From: fresh3nough Date: Sat, 28 Feb 2026 17:21:07 +0000 Subject: [PATCH 1/2] fix(devtools): guard useLayoutEffect against null ref in ContextMenu The useLayoutEffect in ContextMenu accesses ref.current without checking for null. When portalContainer is missing or items is empty, the component returns null (no portal rendered), leaving ref.current as null and causing a crash on the subsequent .contains() call. Guard the effect with the same early-return condition used by the render path (portalContainer == null || items.length === 0) so the effect is a no-op when no portal is mounted. --- .../src/devtools/ContextMenu/ContextMenu.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js index a96634fbe46a..1cafd9ff0314 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js @@ -74,7 +74,13 @@ export default function ContextMenu({ ); useLayoutEffect(() => { - const menu = ((ref.current: any): HTMLElement); + const menu = ref.current; + + // Match the early-return condition below. If neither of these + // is true, menu being null would be a bug. + if (portalContainer == null || items.length === 0) { + return; + } function hideUnlessContains(event: Event) { if (!menu.contains(((event.target: any): Node))) { From 7cf772a49856d6fd1e2bb985a154c946ac665655 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 3 Mar 2026 15:00:55 +0100 Subject: [PATCH 2/2] Fix another crash due to overriding parent ref --- .../src/devtools/ContextMenu/ContextMenu.js | 28 +++++++++++-------- .../ContextMenu/ContextMenuContainer.js | 1 - 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js index 1cafd9ff0314..ded5bbb22e6c 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {useLayoutEffect, createRef} from 'react'; +import {useLayoutEffect} from 'react'; import {createPortal} from 'react-dom'; import ContextMenuItem from './ContextMenuItem'; @@ -16,7 +16,6 @@ import ContextMenuItem from './ContextMenuItem'; import type { ContextMenuItem as ContextMenuItemType, ContextMenuPosition, - ContextMenuRef, } from './types'; import styles from './ContextMenu.css'; @@ -49,7 +48,6 @@ type Props = { items: ContextMenuItemType[], position: ContextMenuPosition, hide: () => void, - ref?: ContextMenuRef, }; export default function ContextMenu({ @@ -57,7 +55,6 @@ export default function ContextMenu({ 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,14 +70,21 @@ export default function ContextMenu({ '[data-react-devtools-portal-root]', ); - useLayoutEffect(() => { - const menu = ref.current; + const hideMenu = portalContainer == null || items.length === 0; + const menuRef = React.useRef(null); - // Match the early-return condition below. If neither of these - // is true, menu being null would be a bug. - if (portalContainer == null || items.length === 0) { + useLayoutEffect(() => { + // 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))) { @@ -104,14 +108,14 @@ export default function ContextMenu({ ownerWindow.removeEventListener('resize', hide); }; - }, []); + }, [hideMenu]); - if (portalContainer == null || items.length === 0) { + if (hideMenu) { return null; } return createPortal( -
+
{items.map(({onClick, content}, index) => ( {content} diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer.js b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer.js index 14e1985b2022..22b70dd447c3 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer.js @@ -53,7 +53,6 @@ export default function ContextMenuContainer({ position={position} hide={hide} items={items} - ref={ref} /> ); }