React Component Patterns
Common React component patterns — composition, render props, HOCs, controlled forms, and error boundaries.
Component Composition
Build UIs from small, focused components that compose together.
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
function App() {
return (
<Card>
<h2>Title</h2>
<p>Content goes here.</p>
</Card>
);
}
Prop Patterns
| Pattern | Description |
|---|---|
| Optional props | Use ? in TypeScript: variant?: "primary" |
| Default props | Destructure with defaults: { variant = "primary" } |
| Spread props | Pass rest: <button {...rest} /> |
| Children | children: React.ReactNode for composition |
| Render props | Pass a function as a child or prop for flexible rendering |
Controlled Components
Form elements whose value is driven by React state.
function SearchForm() {
const [query, setQuery] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(query);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
);
}
Compound Components
Related components that share implicit state through context.
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (id: string) => void;
}>({ activeTab: "", setActiveTab: () => {} });
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
);
}
function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={activeTab === id ? "active" : ""}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
// Usage
<Tabs defaultTab="overview">
<TabList>
<Tab id="overview">Overview</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
</Tabs>
Error Boundaries
Catch rendering errors in a subtree. Must be a class component.
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error("ErrorBoundary caught:", error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>Something went wrong.</p>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Failed to load.</p>}>
<UnstableComponent />
</ErrorBoundary>
Data Fetching Pattern
Fetch data in an effect with loading and error states.
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
try {
setLoading(true);
const data = await fetchUser(userId);
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err as Error);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
}, [userId]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!user) return null;
return <h1>{user.name}</h1>;
}
Key Rules
| Rule | Description |
|---|---|
| One component, one file | Keep components in their own files |
| Named exports | Prefer named exports over default exports |
| Hooks at top level | Never call hooks inside loops, conditions, or nested functions |
| Keys for lists | Always provide stable, unique keys when rendering lists |
| Immutable state | Never mutate state directly — always create new objects/arrays |
| Lift state up | Move shared state to the nearest common ancestor |