Action slot invariants

When building React “action slot” containers (inline vs overflow menus, responsive headers, hold/click controls), enforce these invariants: 1) **Preserve host mount semantics**

copy reviewer prompt

Prompt

Reviewer Prompt

When building React “action slot” containers (inline vs overflow menus, responsive headers, hold/click controls), enforce these invariants:

1) Preserve host mount semantics

  • Don’t render the same children in multiple parallel trees just to measure layout.
  • Avoid unmounting/remounting stateful host actions during responsive collapse/expand if the host’s state/subscriptions must persist.

2) Preserve DOM identity for event guarantees

  • For interactions relying on pointer capture, focus, or other event invariants, keep the captured element mounted (or handle release/cancel at a higher level like document).

3) Normalize host output before applying asChild/DOM-level props

  • If the host can return <>...</> Fragments (or nested Fragments), flatten/normalize to concrete elements before wrapping with <DropdownMenuItem asChild> (or before cloning/annotating).

4) Use the menu framework’s item components for accessibility + behavior

  • Don’t place raw action buttons directly under DropdownMenuContent. Wrap each action with the framework’s item component (e.g., DropdownMenuItem asChild) so roles/keyboard navigation and menu auto-close work consistently.

5) Don’t rely on custom components forwarding unknown props

  • Avoid cloneElement to inject data-* or handlers unless you can guarantee the host component forwards unknown props via ...rest.
  • Prefer wrapping with a simple DOM element (e.g., <span style={{display:'contents'}} data-...>...</span>) so your attributes always land in the DOM.

Example pattern (flatten + menu items + safe wrappers, no Fragment targets):

import { Children, Fragment, isValidElement, useMemo } from 'react';

function flattenToElements(node: React.ReactNode) {
  const out: React.ReactElement[] = [];
  for (const child of Children.toArray(node)) {
    if (!isValidElement(child)) continue;
    if (child.type === Fragment) {
      out.push(...flattenToElements((child.props as any).children));
    } else {
      out.push(child);
    }
  }
  return out;
}

function OverflowMenu({ children }: { children: React.ReactNode }) {
  const elements = useMemo(() => flattenToElements(children), [children]);

  return (
    <DropdownMenuContent>
      {elements.map((el, i) => (
        <DropdownMenuItem key={el.key ?? i} asChild>
          <span style={{ display: 'contents' }} data-action-index={String(i)}>
            {el}
          </span>
        </DropdownMenuItem>
      ))}
    </DropdownMenuContent>
  );
}

Applying these rules prevents: duplicated subscriptions (double mount), broken menu roles/keyboard behavior (raw children), invalid DOM prop application to Fragments, click-proxy failures from missing prop forwarding, and interaction bugs from DOM identity changes during state transitions.

Source discussions