React Patterns Skill
Modern React 19 patterns for scalable applications
PRINCIPLES
- •Composition over inheritance: Build from small, focused components
- •Single responsibility: One component, one job
- •Lift state up: Share state via closest common ancestor
- •Colocation: Keep state close to where it's used
HOOKS PATTERNS
useState Best Practices
typescript
// ✅ Functional updates for derived state
const [count, setCount] = useState(0);
setCount(prev => prev + 1); // Always use prev when depending on current
// ✅ Lazy initialization for expensive computations
const [data, setData] = useState(() => {
return expensiveComputation(); // Only runs once
});
// ✅ Object state with proper updates
const [form, setForm] = useState({ name: '', email: '' });
setForm(prev => ({ ...prev, name: 'John' })); // Merge, don't replace
useEffect Patterns
typescript
// ✅ Cleanup pattern
useEffect(() => {
const subscription = api.subscribe(handler);
return () => {
subscription.unsubscribe(); // Always cleanup
};
}, [handler]);
// ✅ Abort controller for fetch
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, []);
// ✅ Sync external system
useEffect(() => {
const map = mapRef.current;
map.setCenter(center);
}, [center]);
useCallback & useMemo
typescript
// ✅ useCallback for stable function reference
const handleSubmit = useCallback((data: FormData) => {
onSubmit(data);
}, [onSubmit]);
// ✅ useMemo for expensive calculations
const sortedItems = useMemo(() => {
return items.slice().sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// ✅ useMemo for stable object reference
const style = useMemo(() => ({
color: theme.primary,
fontSize: size,
}), [theme.primary, size]);
useTransition (React 19)
typescript
'use client';
import { useTransition } from 'react';
function SearchResults({ query }: { query: string }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState<Result[]>([]);
const handleSearch = (newQuery: string) => {
// Mark as non-urgent update
startTransition(async () => {
const data = await searchAPI(newQuery);
setResults(data);
});
};
return (
<div>
<input onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <ResultList results={results} />}
</div>
);
}
Custom Hooks
typescript
// ✅ Extract reusable logic
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
setStoredValue(prev => {
const valueToStore = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
}, [key]);
return [storedValue, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
COMPONENT PATTERNS
Compound Components
typescript
// Compound component pattern for related UI
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ value, children }: { value: string; children: ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('Tab must be inside Tabs');
return (
<button
role="tab"
aria-selected={context.activeTab === value}
onClick={() => context.setActiveTab(value)}
>
{children}
</button>
);
}
function TabPanel({ value, children }: { value: string; children: ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabPanel must be inside Tabs');
if (context.activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage
<Tabs defaultTab="profile">
<Tabs.List>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="profile">Profile content</Tabs.Panel>
<Tabs.Panel value="settings">Settings content</Tabs.Panel>
</Tabs>
Render Props
typescript
// Render prop pattern for flexible rendering
interface FetchProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => ReactNode;
}
function Fetch<T>({ url, children }: FetchProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return <>{children(data, loading, error)}</>;
}
// Usage
<Fetch<User[]> url="/api/users">
{(users, loading, error) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={users!} />;
}}
</Fetch>
Slot Pattern
typescript
// Slot pattern for flexible layouts
interface CardProps {
children: ReactNode;
header?: ReactNode;
footer?: ReactNode;
}
function Card({ children, header, footer }: CardProps) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card
header={<h2>Title</h2>}
footer={<Button>Save</Button>}
>
Card content here
</Card>
PERFORMANCE PATTERNS
React.memo
typescript
// Memoize when props rarely change
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
// Custom comparison function
const UserCard = memo(
function UserCard({ user, onSelect }: Props) {
return <div onClick={() => onSelect(user.id)}>{user.name}</div>;
},
(prev, next) => prev.user.id === next.user.id
);
Code Splitting
typescript
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<Header />
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
</div>
);
}
// Route-based splitting (Next.js handles this)
const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <Skeleton />,
ssr: false, // Client-only
});
Virtual Lists
typescript
// For long lists, use virtualization
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
STATE MANAGEMENT
Context Pattern
typescript
// Create typed context with reducer
interface State {
user: User | null;
theme: 'light' | 'dark';
}
type Action =
| { type: 'SET_USER'; payload: User }
| { type: 'LOGOUT' }
| { type: 'TOGGLE_THEME' };
const AppContext = createContext<{
state: State;
dispatch: Dispatch<Action>;
} | null>(null);
function appReducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'LOGOUT':
return { ...state, user: null };
case 'TOGGLE_THEME':
return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
default:
return state;
}
}
export function AppProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(appReducer, { user: null, theme: 'light' });
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (!context) throw new Error('useApp must be inside AppProvider');
return context;
}
ANTI-PATTERNS
❌ Avoid
typescript
// ❌ Derived state in useState
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]); // Redundant!
// ❌ Object identity in deps
useEffect(() => {
// Runs every render!
}, [{ id: 1 }]);
// ❌ Missing cleanup
useEffect(() => {
window.addEventListener('resize', handler);
// No cleanup!
}, []);
// ❌ State for everything
const [x, setX] = useState(props.x); // Just use props.x!
✅ Prefer
typescript
// ✅ Derive state from existing
const filteredItems = useMemo(
() => items.filter(i => i.active),
[items]
);
// ✅ Stable reference in deps
const config = useMemo(() => ({ id: 1 }), []);
useEffect(() => {}, [config]);
// ✅ Always cleanup
useEffect(() => {
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, [handler]);
// ✅ Use props directly when possible
function Component({ x }: Props) {
return <div>{x}</div>; // No state needed
}
QUICK REFERENCE
| Pattern | When to Use |
|---|---|
| useState | Local component state |
| useReducer | Complex state logic |
| useContext | Global/shared state |
| useMemo | Expensive calculations |
| useCallback | Stable function references |
| useTransition | Non-urgent updates |
| memo | Prevent unnecessary rerenders |
| Compound Components | Related UI components |
| Render Props | Flexible rendering logic |