skills/petekp/claude-code-setup/react-component-dev

react-component-dev

SKILL.md

React Component Development

Patterns for building composable, accessible, well-structured React components.

Core Principles

  1. Composition over configuration - Props enable customization, not enumerate options
  2. Forwarding refs - Components that render DOM elements forward refs
  3. Accessibility first - Keyboard, screen readers, reduced motion
  4. Predictable APIs - Consistent prop patterns across components

Component Template

import { forwardRef, type ComponentPropsWithoutRef } from "react"
import { cn } from "@/lib/utils"

type ButtonProps = ComponentPropsWithoutRef<"button"> & {
  variant?: "default" | "outline" | "ghost"
  size?: "sm" | "md" | "lg"
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = "default", size = "md", ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={cn(
          "base-styles",
          variantStyles[variant],
          sizeStyles[size],
          className
        )}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, type ButtonProps }

Prop Design

Always Include

Prop Type Purpose
className string Style composition
children ReactNode Content (when applicable)
...rest native props Forward all valid HTML attributes

Variant Props

// Good: Union of literal types
variant?: "default" | "destructive" | "outline"

// Bad: Boolean props that multiply
isPrimary?: boolean
isDestructive?: boolean
isOutline?: boolean

Render Props / Slots

For complex customization:

type DialogProps = {
  trigger?: ReactNode
  title: ReactNode
  description?: ReactNode
  children: ReactNode
  footer?: ReactNode
}

forwardRef Patterns

When to Use

  • Component renders a single DOM element
  • Component wraps another forwardRef component
  • Users might need to call .focus(), measure, or attach refs

When to Skip

  • Component renders multiple root elements
  • Component is purely logic (hooks)
  • Internal-only component never exposed to consumers

Extracting Ref Type

// From DOM element
forwardRef<HTMLDivElement, Props>

// From another component
forwardRef<ComponentRef<typeof OtherComponent>, Props>

File Organization

components/
└── button/
    ├── index.ts              # Re-export: export { Button } from "./button"
    ├── button.tsx            # Implementation
    ├── button.test.tsx       # Tests
    └── use-button-state.ts   # Complex state logic (if needed)

index.ts Pattern

export { Button, type ButtonProps } from "./button"

Keep index.ts as pure re-exports. No logic.

Accessibility Checklist

Keyboard

  • All interactive elements focusable
  • Focus order matches visual order
  • Focus visible (outline or ring)
  • Escape closes modals/dropdowns
  • Enter/Space activates buttons
  • Arrow keys for menu navigation

ARIA

// Buttons with icons only
<button aria-label="Close dialog">
  <XIcon aria-hidden="true" />
</button>

// Loading states
<button disabled aria-busy={isLoading}>
  {isLoading ? <Spinner /> : "Submit"}
</button>

// Expandable content
<button aria-expanded={isOpen} aria-controls="panel-id">
  Toggle
</button>

Reduced Motion

const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)")

// Or in CSS
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; }
}

State Management

Local State

Use useState for:

  • UI state (open/closed, selected)
  • Form inputs (controlled)
  • Ephemeral data (hover, focus)

Derived State

// Bad: useEffect to sync
const [fullName, setFullName] = useState("")
useEffect(() => {
  setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])

// Good: useMemo
const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
)

// Best: Just compute it (if cheap)
const fullName = `${firstName} ${lastName}`

Complex State

// useReducer for multi-field updates
const [state, dispatch] = useReducer(reducer, initialState)

// Or extract to custom hook
const dialog = useDialogState()

Event Handlers

Prop Naming

// Internal handler
const handleClick = () => { ... }

// Prop callbacks: on[Event]
type Props = {
  onClick?: () => void
  onOpenChange?: (open: boolean) => void
  onValueChange?: (value: string) => void
}

Composing Handlers

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ onClick, ...props }, ref) => {
    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
      // Internal logic
      trackClick()
      // Call user's handler
      onClick?.(e)
    }

    return <button ref={ref} onClick={handleClick} {...props} />
  }
)

Testing Approach

What to Test

  • User interactions (click, type, submit)
  • Accessibility (keyboard nav, ARIA states)
  • Conditional rendering
  • Error states

What NOT to Test

  • Implementation details (internal state values)
  • Styling (unless critical to function)
  • Third-party library internals

Test Structure

describe("Button", () => {
  it("calls onClick when clicked", async () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)

    await userEvent.click(screen.getByRole("button"))

    expect(handleClick).toHaveBeenCalledOnce()
  })

  it("is disabled when disabled prop is true", () => {
    render(<Button disabled>Disabled</Button>)

    expect(screen.getByRole("button")).toBeDisabled()
  })
})

Anti-Patterns

Prop Drilling

// Bad: Passing props through many layers
<Parent value={x} onChange={y}>
  <Child value={x} onChange={y}>
    <GrandChild value={x} onChange={y} />

// Better: Context for deep trees
<ValueContext.Provider value={{ x, onChange: y }}>
  <Parent>
    <Child>
      <GrandChild /> {/* useContext inside */}

Premature Abstraction

// Bad: Generic component nobody asked for
<FlexContainer direction="column" gap={4} align="center" justify="between">

// Good: Specific component for the use case
<CardHeader>

Boolean Prop Explosion

// Bad
<Button primary large disabled loading>

// Good
<Button variant="primary" size="lg" disabled isLoading>

Quick Reference

Pattern When
forwardRef Wrapping DOM elements
ComponentPropsWithoutRef<"tag"> Inheriting native props
cn() Merging classNames
as const Literal type inference
useImperativeHandle Custom ref APIs (rare)
React.Children Manipulating children (avoid if possible)
Weekly Installs
7
GitHub Stars
19
First Seen
Jan 20, 2026
Installed on
claude-code5
github-copilot5
codex5
opencode4
gemini-cli4
kilo3