Skip to content

Custom page labelIcon doesn't render on React 19 (ref collision in ExternalElementMounter) #8512

@adri1wald

Description

@adri1wald

Preliminary Checks

Description

Icons passed via labelIcon on OrganizationSwitcher.OrganizationProfilePage (and UserButton.UserProfilePage, same code path) don't render. The cl-navbarButtonIcon__custom-page-N divs are in the DOM but completely empty. Built-in pages (General, Members) work fine.

We traced this through the Clerk source and it's a React 19 ref collision in ExternalElementMounter.

Root cause

Here's the rendering chain for custom page icons:

  1. makeCustomizable is a forwardRef HOC. It wraps Primitives.Icon, creates the cl-navbarButtonIcon wrapper div, and forwards a ref down.
  2. Primitives.Icon destructures { icon: Icon, ...rest } and renders <Icon {...filterProps(rest)} css={...} />. rest contains the forwarded ref.
  3. For custom pages, Icon is the arrow function (props) => <ExternalElementMounter mount={mountIcon} unmount={unmountIcon} {...props} />. It passes everything through, including ref.
  4. ExternalElementMounter destructures { mount, unmount, ...rest } and renders <div ref={nodeRef} {...rest} />.

In React 19, ref is a regular prop. <div ref={nodeRef} {...rest} /> compiles to createElement('div', { ref: nodeRef, ...rest }). The spread overwrites nodeRef with the forwarded ref. nodeRef.current stays null. The useEffect guard if (nodeRef.current) fails. mountIcon() is never called. The host tree's portal nodeMap stays empty. No icon renders.

In React 18 this worked because React refused to pass ref into the arrow function (not wrapped in forwardRef), silently dropped it, and logged a warning. So rest never contained ref.

Built-in icons are unaffected because they're SVG components rendered directly by Primitives.Icon, no ExternalElementMounter in the chain.

Proof

We verified this with three test cases against the Clerk source at @clerk/nextjs@7.2.3:

Scenario mount called?
React 19, no fix 0 times (broken)
React 19, with fix 1 time (works)
React 18, no fix 1 time (works, with "Function components cannot be given refs" warning)

Suggested fix

In packages/ui/src/utils/ExternalElementMounter.tsx, extract ref from rest before spreading:

export const ExternalElementMounter = ({ mount, unmount, ref: _forwardedRef, ...rest }: ExternalElementMounterProps) => {
  const nodeRef = useRef(null);

  useEffect(() => {
    let elRef: HTMLDivElement | undefined;
    if (nodeRef.current) {
      elRef = nodeRef.current;
      mount(nodeRef.current);
    }
    return () => {
      unmount(elRef);
    };
  }, [nodeRef.current]);

  return (
    <div
      ref={nodeRef}
      {...rest}
    />
  );
};

Related

#8447 reports the same symptom for UserButton.UserProfilePage with ui provided. Same root cause.

Environment

@clerk/nextjs: 7.2.3
@clerk/react: 6.4.2
react: 19.1.0
react-dom: 19.1.0
Next.js: 15.3.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions