Loading...
Loading...
Build React components with proper patterns, accessibility, and composition. Use when creating new components, refactoring existing ones, or reviewing component architecture. Covers forwardRef, prop design, accessibility, file organization, and testing approaches.
npx skill4agent add petekp/claude-code-setup react-component-devimport { 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 | Type | Purpose |
|---|---|---|
| | Style composition |
| | Content (when applicable) |
| native props | Forward all valid HTML attributes |
// Good: Union of literal types
variant?: "default" | "destructive" | "outline"
// Bad: Boolean props that multiply
isPrimary?: boolean
isDestructive?: boolean
isOutline?: booleantype DialogProps = {
trigger?: ReactNode
title: ReactNode
description?: ReactNode
children: ReactNode
footer?: ReactNode
}.focus()// From DOM element
forwardRef<HTMLDivElement, Props>
// From another component
forwardRef<ComponentRef<typeof OtherComponent>, Props>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)export { Button, type ButtonProps } from "./button"// 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>const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)")
// Or in CSS
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}useState// 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}`// useReducer for multi-field updates
const [state, dispatch] = useReducer(reducer, initialState)
// Or extract to custom hook
const dialog = useDialogState()// Internal handler
const handleClick = () => { ... }
// Prop callbacks: on[Event]
type Props = {
onClick?: () => void
onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}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} />
}
)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()
})
})// 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 */}// Bad: Generic component nobody asked for
<FlexContainer direction="column" gap={4} align="center" justify="between">
// Good: Specific component for the use case
<CardHeader>// Bad
<Button primary large disabled loading>
// Good
<Button variant="primary" size="lg" disabled isLoading>| Pattern | When |
|---|---|
| Wrapping DOM elements |
| Inheriting native props |
| Merging classNames |
| Literal type inference |
| Custom ref APIs (rare) |
| Manipulating children (avoid if possible) |