React Component Standards¶
React Component Standards¶
Component Structure¶
Follow this order within every component file:
1. Imports — external first, then internal, then types:
import { useState, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/components/ui/Button';
import { useAuth } from '@/hooks/useAuth';
import type { User } from '@/types';
2. Types/Interfaces — define props before the component:
3. Component body — hooks, derived state, callbacks, then render:
export function UserCard({ user, onEdit, className }: UserCardProps) {
// Hooks first
const [isExpanded, setIsExpanded] = useState(false);
const { isAdmin } = useAuth();
// Derived state
const canEdit = isAdmin || user.isOwner;
// Callbacks
const handleEdit = useCallback(() => {
onEdit?.(user);
}, [onEdit, user]);
// Render
return (
<div className={cn('rounded-lg border p-4', className)}>
<h3>{user.name}</h3>
{canEdit && <Button onClick={handleEdit}>Edit</Button>}
</div>
);
}
Server vs Client Components¶
| Component Type | Use For |
|---|---|
| Server (default) | Data fetching, static content, no interactivity |
Client ('use client') |
Event handlers, hooks, browser APIs |
// Server Component (default) - can fetch data directly
async function UserList() {
const users = await getUsers(); // Direct async/await
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Client Component - needs 'use client' directive
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Props Typing¶
Always type props explicitly:
// Good - explicit interface
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
// With ref forwarding
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'bordered';
}
const Card = forwardRef<HTMLDivElement, CardProps>(
({ variant = 'default', className, ...props }, ref) => (
<div ref={ref} className={cn(variants[variant], className)} {...props} />
)
);
Composition over Prop Drilling¶
Prefer composition patterns:
// Bad - prop drilling
function Page({ user, theme, locale }) {
return <Layout user={user} theme={theme} locale={locale}>
<Sidebar user={user} theme={theme} />
<Content user={user} theme={theme} locale={locale} />
</Layout>;
}
// Good - composition
function Page() {
return (
<Layout
sidebar={<Sidebar />}
content={<Content />}
/>
);
}
// Good - context for truly global state
function Page() {
return (
<UserProvider>
<ThemeProvider>
<Layout />
</ThemeProvider>
</UserProvider>
);
}
Named Exports¶
Prefer named exports over default exports:
// Good - named export
export function UserCard() { ... }
export function UserList() { ... }
// Avoid - default export
export default function UserCard() { ... }
Named exports enable: - Consistent naming across imports - Better IDE autocomplete - Easier refactoring