RE
Component Patterns
React core v1.0.0
React Component Patterns
Overview
React component patterns establish reusable, maintainable, and testable UI building blocks. This skill covers composition patterns, compound components, render props, hooks, and higher-order components.
Key Concepts
Component Architecture
┌─────────────────────────────────────────────────────────────┐
│ React Component Hierarchy │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ App (Root) │ │
│ │ • Global state providers │ │
│ │ • Theme, Auth, Router context │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Layout │ │ Pages │ │ Shared │ │
│ │ │ │ │ │Components│ │
│ │Header │ │Dashboard │ │ Button │ │
│ │Sidebar │ │ Profile │ │ Modal │ │
│ │Footer │ │ Settings │ │ Form │ │
│ └────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Features │ │ Domain │ │
│ │ │ │Components│ │
│ │UserTable │ │OrderCard │ │
│ │OrderList │ │CartItem │ │
│ └──────────┘ └──────────┘ │
│ │
│ Component Types: │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Presentational: UI only, receive data via props │ │
│ │ Container: Business logic, data fetching │ │
│ │ Layout: Page structure, positioning │ │
│ │ Feature: Complete functionality, domain-specific │ │
│ │ Utility: Higher-order, providers, error boundaries │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Best Practices
1. Prefer Composition Over Inheritance
Use props.children and slots for flexible components.
2. Keep Components Small and Focused
Single responsibility - one component, one job.
3. Lift State Only When Necessary
Start with local state, lift when sharing is needed.
4. Use TypeScript for Props
Define explicit interfaces for all component props.
5. Separate Concerns with Custom Hooks
Extract reusable logic into custom hooks.
Code Examples
Example 1: Compound Components
import React, { createContext, useContext, useState, ReactNode } from 'react';
// Context for compound component state
interface AccordionContextValue {
activeItems: Set<string>;
toggle: (id: string) => void;
allowMultiple: boolean;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordionContext() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within Accordion');
}
return context;
}
// Root component
interface AccordionProps {
children: ReactNode;
allowMultiple?: boolean;
defaultActive?: string[];
}
function Accordion({ children, allowMultiple = false, defaultActive = [] }: AccordionProps) {
const [activeItems, setActiveItems] = useState<Set<string>>(
new Set(defaultActive)
);
const toggle = (id: string) => {
setActiveItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!allowMultiple) {
next.clear();
}
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ activeItems, toggle, allowMultiple }}>
<div className="accordion" role="tablist">
{children}
</div>
</AccordionContext.Provider>
);
}
// Item component
interface AccordionItemProps {
id: string;
children: ReactNode;
}
function AccordionItem({ id, children }: AccordionItemProps) {
const { activeItems } = useAccordionContext();
const isActive = activeItems.has(id);
return (
<div className={`accordion-item ${isActive ? 'active' : ''}`} data-id={id}>
{children}
</div>
);
}
// Header component
interface AccordionHeaderProps {
id: string;
children: ReactNode;
}
function AccordionHeader({ id, children }: AccordionHeaderProps) {
const { activeItems, toggle } = useAccordionContext();
const isActive = activeItems.has(id);
return (
<button
className="accordion-header"
onClick={() => toggle(id)}
aria-expanded={isActive}
aria-controls={`panel-${id}`}
role="tab"
>
{children}
<span className="accordion-icon">{isActive ? '−' : '+'}</span>
</button>
);
}
// Panel component
interface AccordionPanelProps {
id: string;
children: ReactNode;
}
function AccordionPanel({ id, children }: AccordionPanelProps) {
const { activeItems } = useAccordionContext();
const isActive = activeItems.has(id);
if (!isActive) return null;
return (
<div
className="accordion-panel"
id={`panel-${id}`}
role="tabpanel"
aria-hidden={!isActive}
>
{children}
</div>
);
}
// Attach subcomponents
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;
// Usage
function FAQSection() {
return (
<Accordion allowMultiple defaultActive={['faq-1']}>
<Accordion.Item id="faq-1">
<Accordion.Header id="faq-1">What is React?</Accordion.Header>
<Accordion.Panel id="faq-1">
React is a JavaScript library for building user interfaces.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="faq-2">
<Accordion.Header id="faq-2">What are hooks?</Accordion.Header>
<Accordion.Panel id="faq-2">
Hooks let you use state and other React features without classes.
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}
Example 2: Render Props Pattern
import React, { useState, useCallback, ReactNode } from 'react';
// Render props for mouse position
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
children: (position: MousePosition) => ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
const handleMouseMove = useCallback((event: React.MouseEvent) => {
setPosition({
x: event.clientX,
y: event.clientY,
});
}, []);
return (
<div onMouseMove={handleMouseMove} style={{ height: '100%' }}>
{children(position)}
</div>
);
}
// Render props for async data fetching
interface AsyncDataProps<T> {
url: string;
children: (state: {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}) => ReactNode;
}
function AsyncData<T>({ url, children }: AsyncDataProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
}, [url]);
React.useEffect(() => {
fetchData();
}, [fetchData]);
return <>{children({ data, loading, error, refetch: fetchData })}</>;
}
// Usage
function UserProfile({ userId }: { userId: string }) {
return (
<AsyncData<User> url={`/api/users/${userId}`}>
{({ data: user, loading, error, refetch }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} retry={refetch} />;
if (!user) return null;
return (
<div className="user-profile">
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}}
</AsyncData>
);
}
Example 3: Custom Hooks
import { useState, useEffect, useCallback, useRef } from 'react';
// Debounced value hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Local storage hook with sync
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
// Get initial value from storage or use default
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
// Sync with localStorage
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const valueToStore = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
},
[key]
);
// Listen for changes from other tabs
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
setStoredValue(JSON.parse(e.newValue));
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
return [storedValue, setValue];
}
// Async hook with loading and error states
interface UseAsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
interface UseAsyncReturn<T> extends UseAsyncState<T> {
execute: () => Promise<void>;
reset: () => void;
}
function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate = true
): UseAsyncReturn<T> {
const [state, setState] = useState<UseAsyncState<T>>({
data: null,
loading: immediate,
error: null,
});
const execute = useCallback(async () => {
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const data = await asyncFunction();
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
}, [asyncFunction]);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { ...state, execute, reset };
}
// Usage
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const [preferences, setPreferences] = useLocalStorage('search-prefs', {
sortBy: 'relevance',
limit: 10,
});
const searchFn = useCallback(
() => searchAPI(debouncedQuery, preferences),
[debouncedQuery, preferences]
);
const { data: results, loading, error, execute: search } = useAsync(
searchFn,
false
);
useEffect(() => {
if (debouncedQuery) {
search();
}
}, [debouncedQuery, search]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{loading && <Spinner />}
{error && <ErrorMessage error={error} />}
{results && <ResultsList results={results} />}
</div>
);
}
Example 4: Higher-Order Components
import React, { ComponentType, useEffect } from 'react';
// HOC for authentication
interface WithAuthProps {
user: User;
}
function withAuth<P extends WithAuthProps>(
WrappedComponent: ComponentType<P>
): ComponentType<Omit<P, keyof WithAuthProps>> {
return function WithAuthComponent(props: Omit<P, keyof WithAuthProps>) {
const { user, loading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && !user) {
navigate('/login');
}
}, [user, loading, navigate]);
if (loading) {
return <LoadingScreen />;
}
if (!user) {
return null;
}
return <WrappedComponent {...(props as P)} user={user} />;
};
}
// HOC for error boundary
interface WithErrorBoundaryOptions {
fallback?: React.ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
function withErrorBoundary<P>(
WrappedComponent: ComponentType<P>,
options: WithErrorBoundaryOptions = {}
): ComponentType<P> {
return class ErrorBoundary extends React.Component<P, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
options.onError?.(error, errorInfo);
}
render() {
if (this.state.hasError) {
return options.fallback || <div>Something went wrong</div>;
}
return <WrappedComponent {...this.props} />;
}
};
}
// HOC for feature flags
function withFeatureFlag<P>(
WrappedComponent: ComponentType<P>,
flagName: string,
fallback: React.ReactNode = null
): ComponentType<P> {
return function WithFeatureFlag(props: P) {
const { isEnabled } = useFeatureFlag(flagName);
if (!isEnabled) {
return <>{fallback}</>;
}
return <WrappedComponent {...props} />;
};
}
// Usage - compose HOCs
const ProtectedDashboard = withAuth(
withErrorBoundary(
withFeatureFlag(Dashboard, 'new-dashboard'),
{ fallback: <ErrorFallback /> }
)
);
Example 5: Polymorphic Components
import React, { ElementType, ComponentPropsWithoutRef, ReactNode } from 'react';
// Polymorphic component type
type PolymorphicProps<E extends ElementType, Props = {}> = Props & {
as?: E;
children?: ReactNode;
} & Omit<ComponentPropsWithoutRef<E>, keyof Props | 'as' | 'children'>;
// Button that can render as different elements
interface ButtonBaseProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
type ButtonProps<E extends ElementType = 'button'> = PolymorphicProps<E, ButtonBaseProps>;
function Button<E extends ElementType = 'button'>({
as,
variant = 'primary',
size = 'md',
loading = false,
children,
className,
disabled,
...props
}: ButtonProps<E>) {
const Component = as || 'button';
const classNames = [
'btn',
`btn-${variant}`,
`btn-${size}`,
loading && 'btn-loading',
className,
]
.filter(Boolean)
.join(' ');
return (
<Component
className={classNames}
disabled={disabled || loading}
{...props}
>
{loading && <Spinner size="sm" />}
{children}
</Component>
);
}
// Usage
function Navigation() {
return (
<nav>
{/* Renders as button */}
<Button onClick={handleClick}>Click me</Button>
{/* Renders as anchor */}
<Button as="a" href="/dashboard">
Go to Dashboard
</Button>
{/* Renders as React Router Link */}
<Button as={Link} to="/profile" variant="secondary">
Profile
</Button>
</nav>
);
}
// Text component with polymorphism
interface TextBaseProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
color?: 'primary' | 'secondary' | 'muted' | 'error';
}
type TextProps<E extends ElementType = 'span'> = PolymorphicProps<E, TextBaseProps>;
function Text<E extends ElementType = 'span'>({
as,
size = 'md',
weight = 'normal',
color = 'primary',
children,
className,
...props
}: TextProps<E>) {
const Component = as || 'span';
return (
<Component
className={`text text-${size} text-${weight} text-${color} ${className || ''}`}
{...props}
>
{children}
</Component>
);
}
// Usage
function Article() {
return (
<article>
<Text as="h1" size="xl" weight="bold">
Article Title
</Text>
<Text as="p" color="muted">
Published on January 1, 2025
</Text>
<Text as="div" size="lg">
Article content...
</Text>
</article>
);
}
Anti-Patterns
❌ Prop Drilling
// WRONG - passing props through many levels
function App() {
const [user, setUser] = useState<User | null>(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Header user={user} setUser={setUser} />;
}
// ✅ CORRECT - use context
const UserContext = createContext<UserContextValue | null>(null);
function App() {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}
❌ Massive Components
Split large components into smaller, focused ones with single responsibilities.