Files
v10/.claude/skills/component/references/anti-patterns.md
T

7.1 KiB

Component Anti-Patterns

Common mistakes when building UI components.

Prop Explosion

// BAD: 30+ props on one component
<Dialog
  open={open}
  onOpenChange={setOpen}
  title="Title"
  description="Desc"
  closeButton={true}
  overlayClassName="..."
  contentClassName="..."
  // ... 20 more props
/>

// GOOD: Compound components
<Dialog.Root open={open} onOpenChange={setOpen}>
  <Dialog.Overlay className="..." />
  <Dialog.Content className="...">
    <Dialog.Title>Title</Dialog.Title>
    <Dialog.Close>Close</Dialog.Close>
  </Dialog.Content>
</Dialog.Root>

Why it fails: Inflexible, hard to style parts independently, poor TypeScript experience.


Inline Styles for State

// BAD: Forces JS styling, fights theming
<button style={{ opacity: disabled ? 0.5 : 1 }}>

// GOOD: Data attributes for CSS
<button data-disabled={disabled || undefined}>
[data-disabled] {
  opacity: 0.5;
}

Why it fails: Specificity issues, can't be themed, harder to override.


Shipping CSS in Core

// BAD: Specificity wars, hard to override
import '@lib/button/styles.css';

// GOOD: Headless — user brings styles
import { Button } from '@lib/button';

Why it fails: CSS conflicts, specificity battles, theming difficulties.


Unnecessary Render Function Wrappers

// BAD: Function wrapper just passes props through
<PlayButton
  render={(props) => (
    <Button {...props} className="play-btn">
      <PlayIcon />
    </Button>
  )}
/>

// BAD: Trivial passthrough
<TimeSlider.Root render={(props) => <SliderRoot {...props} />} />

// BAD: Passing component reference directly — breaks hooks reconciliation
<PlayButton render={Button} />

// GOOD: Element form — className and children on the headless component
<PlayButton className="play-btn" render={<Button />}>
  <PlayIcon />
</PlayButton>

// GOOD: Element form for passthrough
<TimeSlider.Root render={<SliderRoot />} />

// GOOD: Element form with extra props on render target
<PlayButton className="play-btn" render={<Button variant="icon" />}>
  <PlayIcon />
</PlayButton>

Why it fails: Function wrappers add indirection, noise, and implicit any on props parameter. Component references (render={Component}) bypass React's reconciliation — components are called as plain functions, breaking hooks. Use element form (render={<Component />}) or function form only when rendering a different element type or accessing component state.


The as Prop

// BAD: TypeScript performance issues
<Button as="a" href="/home">Link</Button>

// GOOD: render prop
<Button render={<a href="/home" />}>Link</Button>

// GOOD: asChild for simple cases
<Button asChild>
  <a href="/home">Link</a>
</Button>

Why it fails: Complex generic types slow TypeScript language server and IDE.


Hidden Prop Flow with asChild

// BAD: Which props does Button receive?
<Dialog.Trigger asChild>
  <Button onClick={myHandler}>Open</Button>
</Dialog.Trigger>

// GOOD: Explicit prop handling
<Dialog.Trigger
  render={(props) => (
    <Button {...props} onClick={(e) => {
      myHandler(e);
      props.onClick?.(e);
    }}>
      Open
    </Button>
  )}
/>

Why it fails: Debugging difficult, behavior non-obvious, silent breakage.


No State Access in Polymorphism

// BAD: asChild can't access internal state
<Switch.Thumb asChild>
  <span>{/* How to show different icon when checked? */}</span>
</Switch.Thumb>

// GOOD: render function provides state
<Switch.Thumb
  render={(props, state) => (
    <span {...props}>
      {state.checked ? <CheckIcon /> : <XIcon />}
    </span>
  )}
/>

Why it fails: Can't conditionally render based on component state.


Missing Controlled Support

// BAD: Only uncontrolled
<Accordion defaultValue="item-1">

// GOOD: Both modes
<Accordion defaultValue="item-1">  {/* uncontrolled */}
<Accordion value={value} onValueChange={setValue}>  {/* controlled */}

Why it fails: Users can't integrate with external state management.


Context Without Scoping

// BAD: Nested components collide
<Dialog.Root>
  <Dialog.Content>
    <Dialog.Root>
      {' '}
      {/* Reads parent's context! */}
      <Dialog.Content />
    </Dialog.Root>
  </Dialog.Content>
</Dialog.Root>

// GOOD: Scoped contexts — each Root creates new scope

Why it fails: Nested instances interfere with each other.


No Exit Animation Support

// BAD: Element unmounts immediately
{open && <Dialog.Content>...</Dialog.Content>}

// GOOD: CSS exit animation with data attributes
<Dialog.Content data-state={open ? 'open' : 'closed'}>

// GOOD: JS animation with keepMounted
<Dialog.Portal keepMounted>
  <AnimatePresence>
    {open && <Dialog.Content />}
  </AnimatePresence>
</Dialog.Portal>

Why it fails: No opportunity to animate out before removal.


Ignoring SSR

// BAD: document undefined on server
function Portal({ children }) {
  return createPortal(children, document.body);
}

// GOOD: Wait for mount
function Portal({ children }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);

  if (!mounted) return <>{children}</>;
  return createPortal(children, document.body);
}

Why it fails: document doesn't exist during SSR.


Focus Not Managed

// BAD: Focus lost when dialog opens
<Dialog open={open}>
  <Dialog.Content>...</Dialog.Content>
</Dialog>

// GOOD: Focus trapped and restored
<Dialog open={open}>
  <Dialog.Content>
    <FocusScope trapped restoreFocus>
      ...
    </FocusScope>
  </Dialog.Content>
</Dialog>

Why it fails: Keyboard users can navigate outside modal, focus lost on close.


Non-Standard Attribute Names

// BAD: Inconsistent naming
<Component isOpen={true} isDisabled={false}>

// GOOD: Consistent data attributes
<Component
  data-state={open ? 'open' : 'closed'}
  data-disabled={disabled || undefined}
>

Why it fails: Inconsistent API, harder to style with CSS selectors.


Forgetting Ref Forwarding

// BAD: Ref can't reach DOM element
const Button = ({ children }) => <button>{children}</button>;

// GOOD: Forward ref
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ children, ...props }, ref) => (
  <button ref={ref} {...props}>
    {children}
  </button>
));

Why it fails: Parent components can't access DOM for focus, measurement.


Testing Checklist

  • No prop explosion (use compound components)
  • State styled via data attributes, not inline
  • No CSS shipped in component package
  • Polymorphism via render or asChild, not as
  • No unnecessary render function wrappers (render={<Component />} over render={(props) => <Component {...props} />})
  • Both controlled and uncontrolled modes
  • Nested instances don't interfere
  • Exit animations possible
  • SSR-safe (no document at module scope)
  • Focus managed for modals
  • Refs forwarded to DOM elements

See Also