Bug description
In PopoverTarget (packages/core/src/components/popover-next/popoverTarget.tsx), mergeRefs is called unconditionally in the render body of a functional component:
const ref = mergeRefs(floatingData.refs.setReference, targetRef);
This produces a new ref callback on every render. When React sees a changed ref callback, it invokes the previous one with null and the new one with the DOM node. The setReference(null) triggers a floating-ui state update, which causes a re-render, which creates another new mergeRefs result — producing an infinite loop that crashes with:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
Notably, Blueprint's own JSDoc on mergeRefs already warns about this:
/**
* Utility for merging refs into one singular callback ref.
* If using in a functional component, would recomend using `useMemo` to preserve function identity.
*/
export function mergeRefs<T>(...refs: Array<React.Ref<T> | undefined>): React.RefCallback<T> {
PopoverTarget is a forwardRef functional component but does not follow this guidance.
Steps to reproduce
- Render a
PopoverNext whose parent re-renders frequently (e.g. connected to a Redux store or React Query cache that resolves synchronously on mount)
- The page crashes immediately with a stack trace through
floating-ui.react.mjs → floating-ui setReference → PopoverTarget
The crash is timing-dependent — it requires the parent to re-render fast enough that React's ref cleanup/re-assignment cycle doesn't settle. Synchronous cache hits (React Query, Redux selectors) reliably trigger it.
Suggested fix
Memoize the mergeRefs call in popoverTarget.tsx:
const ref = useMemo(() => mergeRefs(floatingData.refs.setReference, targetRef), [floatingData.refs.setReference, targetRef]);
Environment
@blueprintjs/core 6.9.1 (also confirmed on develop branch HEAD)
- React 18.3.0
@floating-ui/react (as bundled/re-exported by Blueprint)
Bug description
In
PopoverTarget(packages/core/src/components/popover-next/popoverTarget.tsx),mergeRefsis called unconditionally in the render body of a functional component:This produces a new ref callback on every render. When React sees a changed ref callback, it invokes the previous one with
nulland the new one with the DOM node. ThesetReference(null)triggers a floating-ui state update, which causes a re-render, which creates another newmergeRefsresult — producing an infinite loop that crashes with:Notably, Blueprint's own JSDoc on
mergeRefsalready warns about this:PopoverTargetis aforwardReffunctional component but does not follow this guidance.Steps to reproduce
PopoverNextwhose parent re-renders frequently (e.g. connected to a Redux store or React Query cache that resolves synchronously on mount)floating-ui.react.mjs→ floating-uisetReference→PopoverTargetThe crash is timing-dependent — it requires the parent to re-render fast enough that React's ref cleanup/re-assignment cycle doesn't settle. Synchronous cache hits (React Query, Redux selectors) reliably trigger it.
Suggested fix
Memoize the
mergeRefscall inpopoverTarget.tsx:Environment
@blueprintjs/core6.9.1 (also confirmed ondevelopbranch HEAD)@floating-ui/react(as bundled/re-exported by Blueprint)