name: building-components description: Build React components with TypeScript, composition patterns, and consistent structure.
Building Components
Goal
Create well-typed, reusable React function components following composition-first patterns with co-located types, tests, and styles.
When to Use
- Building any new React component (primitive, composite, or page-level)
- Refactoring an existing component to improve type safety or composability
- Reviewing component code for adherence to project conventions
Instructions
1. Component File Structure
Co-locate the component, its types, tests, and barrel export in a single directory:
Button/
├── Button.tsx # Component implementation
├── Button.test.tsx # Tests
└── index.ts # Re-export: export { Button } from "./Button";
2. Define Props with TypeScript Interfaces
Always use interface for component props. Extend native HTML attributes when wrapping primitives:
import { forwardRef, type ButtonHTMLAttributes } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
}
3. Implement Function Components
Use named function declarations. Forward refs on all primitives:
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", size = "md", isLoading = false, children, className, ...rest }, ref) => {
const baseStyles = "inline-flex items-center justify-center rounded font-medium transition";
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
};
const sizeStyles: Record<NonNullable<ButtonProps["size"]>, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
return (
<button
ref={ref}
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className ?? ""}`}
disabled={isLoading || rest.disabled}
{...rest}
>
{isLoading ? <span className="animate-spin mr-2">...</span> : null}
{children}
</button>
);
},
);
Button.displayName = "Button";
export { Button };
4. Composition Over Configuration
Prefer composing small components over adding many props to one component:
// Good: composable
<Card>
<Card.Header>
<Card.Title>Dashboard</Card.Title>
</Card.Header>
<Card.Body>{content}</Card.Body>
</Card>
// Avoid: prop-heavy
<Card title="Dashboard" headerAction={<Button />} body={content} footer={footer} />
5. Component Categories
| Category | Location | Purpose |
|---|---|---|
| Primitives | components/ui/ |
Button, Input, Card, Modal |
| Composites | components/features/ |
UserCard, InvoiceTable, SearchBar |
| Pages | pages/ |
DashboardPage, SettingsPage |
6. Separate Concerns
Keep data fetching out of presentational components. Pass data through props:
// Page fetches, component renders
function UsersPage() {
const { data: users } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
return <UserList users={users ?? []} />;
}
// Pure presentational component
interface UserListProps {
users: User[];
}
function UserList({ users }: UserListProps) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Constraints
✅ Do
- Give each component a single responsibility
- Define explicit prop types using
interface - Use
forwardRefon all primitive UI components - Use composition (children, compound components) for flexibility
- Co-locate component, types, and tests in the same directory
- Use Tailwind utility classes for styling
❌ Don't
- Use
anyorunknownfor prop types - Prop-drill more than 2 levels deep -- lift state or use a store
- Mix data fetching logic into presentational components
- Hardcode user-facing strings (prepare for i18n)
- Use class components
- Create "god components" with more than 200 lines
Output Format
A component directory containing:
ComponentName.tsxwith typed props and implementationindex.tsbarrel exportComponentName.test.tsxplaceholder or full tests
Dependencies
- frontend/scaffolding-frontend/SKILL.md -- project must be scaffolded first
- references/component-patterns.md -- advanced pattern reference