frontend-patterns
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
Frontend Development Patterns
Modern frontend patterns for React, Next.js, and performant user interfaces.
When to Activate
- Building React components (composition, props, rendering)
- Managing state (useState, useReducer, Zustand, Context)
- Implementing data fetching (SWR, React Query, server components)
- Optimizing performance (memoization, virtualization, code splitting)
- Working with forms (validation, controlled inputs, Zod schemas)
- Handling client-side routing and navigation
- Building accessible, responsive UI patterns
Component Patterns
Composition Over Inheritance
// PASS: GOOD: Component compositioninterface CardProps { children: React.ReactNode variant?: 'default' | 'outlined'}
export function Card({ children, variant = 'default' }: CardProps) { return <div className={`card card-${variant}`}>{children}</div>}
export function CardHeader({ children }: { children: React.ReactNode }) { return <div className="card-header">{children}</div>}
export function CardBody({ children }: { children: React.ReactNode }) { return <div className="card-body">{children}</div>}
// Usage<Card> <CardHeader>Title</CardHeader> <CardBody>Content</CardBody></Card>Compound Components
interface TabsContextValue { activeTab: string setActiveTab: (tab: string) => void}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
export function Tabs({ children, defaultTab }: { children: React.ReactNode defaultTab: string}) { const [activeTab, setActiveTab] = useState(defaultTab)
return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> {children} </TabsContext.Provider> )}
export function TabList({ children }: { children: React.ReactNode }) { return <div className="tab-list">{children}</div>}
export function Tab({ id, children }: { id: string, children: React.ReactNode }) { const context = useContext(TabsContext) if (!context) throw new Error('Tab must be used within Tabs')
return ( <button className={context.activeTab === id ? 'active' : ''} onClick={() => context.setActiveTab(id)} > {children} </button> )}
// Usage<Tabs defaultTab="overview"> <TabList> <Tab id="overview">Overview</Tab> <Tab id="details">Details</Tab> </TabList></Tabs>Render Props Pattern
interface DataLoaderProps<T> { url: string children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode}
export function DataLoader<T>({ url, children }: DataLoaderProps<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<DataLoader<Market[]> url="/api/markets"> {(markets, loading, error) => { if (loading) return <Spinner /> if (error) return <Error error={error} /> return <MarketList markets={markets!} /> }}</DataLoader>Custom Hooks Patterns
State Management Hook
export function useToggle(initialValue = false): [boolean, () => void] { const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => { setValue(v => !v) }, [])
return [value, toggle]}
// Usageconst [isOpen, toggleOpen] = useToggle()Async Data Fetching Hook
interface UseQueryOptions<T> { onSuccess?: (data: T) => void onError?: (error: Error) => void enabled?: boolean}
export function useQuery<T>( key: string, fetcher: () => Promise<T>, options?: UseQueryOptions<T>) { const [data, setData] = useState<T | null>(null) const [error, setError] = useState<Error | null>(null) const [loading, setLoading] = useState(false)
const refetch = useCallback(async () => { setLoading(true) setError(null)
try { const result = await fetcher() setData(result) options?.onSuccess?.(result) } catch (err) { const error = err as Error setError(error) options?.onError?.(error) } finally { setLoading(false) } }, [fetcher, options])
useEffect(() => { if (options?.enabled !== false) { refetch() } }, [key, refetch, options?.enabled])
return { data, error, loading, refetch }}
// Usageconst { data: markets, loading, error, refetch } = useQuery( 'markets', () => fetch('/api/markets').then(r => r.json()), { onSuccess: data => console.log('Fetched', data.length, 'markets'), onError: err => console.error('Failed:', err) })Debounce Hook
export function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value) }, delay)
return () => clearTimeout(handler) }, [value, delay])
return debouncedValue}
// Usageconst [searchQuery, setSearchQuery] = useState('')const debouncedQuery = useDebounce(searchQuery, 500)
useEffect(() => { if (debouncedQuery) { performSearch(debouncedQuery) }}, [debouncedQuery])State Management Patterns
Context + Reducer Pattern
interface State { markets: Market[] selectedMarket: Market | null loading: boolean}
type Action = | { type: 'SET_MARKETS'; payload: Market[] } | { type: 'SELECT_MARKET'; payload: Market } | { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State { switch (action.type) { case 'SET_MARKETS': return { ...state, markets: action.payload } case 'SELECT_MARKET': return { ...state, selectedMarket: action.payload } case 'SET_LOADING': return { ...state, loading: action.payload } default: return state }}
const MarketContext = createContext<{ state: State dispatch: Dispatch<Action>} | undefined>(undefined)
export function MarketProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(reducer, { markets: [], selectedMarket: null, loading: false })
return ( <MarketContext.Provider value={{ state, dispatch }}> {children} </MarketContext.Provider> )}
export function useMarkets() { const context = useContext(MarketContext) if (!context) throw new Error('useMarkets must be used within MarketProvider') return context}Performance Optimization
Memoization
// PASS: useMemo for expensive computationsconst sortedMarkets = useMemo(() => { return markets.sort((a, b) => b.volume - a.volume)}, [markets])
// PASS: useCallback for functions passed to childrenconst handleSearch = useCallback((query: string) => { setSearchQuery(query)}, [])
// PASS: React.memo for pure componentsexport const MarketCard = React.memo<MarketCardProps>(({ market }) => { return ( <div className="market-card"> <h3>{market.name}</h3> <p>{market.description}</p> </div> )})Code Splitting & Lazy Loading
import { lazy, Suspense } from 'react'
// PASS: Lazy load heavy componentsconst HeavyChart = lazy(() => import('./HeavyChart'))const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
export function Dashboard() { return ( <div> <Suspense fallback={<ChartSkeleton />}> <HeavyChart data={data} /> </Suspense>
<Suspense fallback={null}> <ThreeJsBackground /> </Suspense> </div> )}Virtualization for Long Lists
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualMarketList({ markets }: { markets: Market[] }) { const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({ count: markets.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, // Estimated row height overscan: 5 // Extra items to render })
return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }} > {virtualizer.getVirtualItems().map(virtualRow => ( <div key={virtualRow.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)` }} > <MarketCard market={markets[virtualRow.index]} /> </div> ))} </div> </div> )}Form Handling Patterns
Controlled Form with Validation
interface FormData { name: string description: string endDate: string}
interface FormErrors { name?: string description?: string endDate?: string}
export function CreateMarketForm() { const [formData, setFormData] = useState<FormData>({ name: '', description: '', endDate: '' })
const [errors, setErrors] = useState<FormErrors>({})
const validate = (): boolean => { const newErrors: FormErrors = {}
if (!formData.name.trim()) { newErrors.name = 'Name is required' } else if (formData.name.length > 200) { newErrors.name = 'Name must be under 200 characters' }
if (!formData.description.trim()) { newErrors.description = 'Description is required' }
if (!formData.endDate) { newErrors.endDate = 'End date is required' }
setErrors(newErrors) return Object.keys(newErrors).length === 0 }
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault()
if (!validate()) return
try { await createMarket(formData) // Success handling } catch (error) { // Error handling } }
return ( <form onSubmit={handleSubmit}> <input value={formData.name} onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="Market name" /> {errors.name && <span className="error">{errors.name}</span>}
{/* Other fields */}
<button type="submit">Create Market</button> </form> )}Error Boundary Pattern
interface ErrorBoundaryState { hasError: boolean error: Error | null}
export class ErrorBoundary extends React.Component< { children: React.ReactNode }, ErrorBoundaryState> { state: ErrorBoundaryState = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error } }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('Error boundary caught:', error, errorInfo) }
render() { if (this.state.hasError) { return ( <div className="error-fallback"> <h2>Something went wrong</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> Try again </button> </div> ) }
return this.props.children }}
// Usage<ErrorBoundary> <App /></ErrorBoundary>Animation Patterns
Framer Motion Animations
import { motion, AnimatePresence } from 'framer-motion'
// PASS: List animationsexport function AnimatedMarketList({ markets }: { markets: Market[] }) { return ( <AnimatePresence> {markets.map(market => ( <motion.div key={market.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > <MarketCard market={market} /> </motion.div> ))} </AnimatePresence> )}
// PASS: Modal animationsexport function Modal({ isOpen, onClose, children }: ModalProps) { return ( <AnimatePresence> {isOpen && ( <> <motion.div className="modal-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} /> <motion.div className="modal-content" initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }} > {children} </motion.div> </> )} </AnimatePresence> )}Accessibility Patterns
Keyboard Navigation
export function Dropdown({ options, onSelect }: DropdownProps) { const [isOpen, setIsOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(0)
const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault() setActiveIndex(i => Math.min(i + 1, options.length - 1)) break case 'ArrowUp': e.preventDefault() setActiveIndex(i => Math.max(i - 1, 0)) break case 'Enter': e.preventDefault() onSelect(options[activeIndex]) setIsOpen(false) break case 'Escape': setIsOpen(false) break } }
return ( <div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" onKeyDown={handleKeyDown} > {/* Dropdown implementation */} </div> )}Focus Management
export function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef<HTMLDivElement>(null) const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => { if (isOpen) { // Save currently focused element previousFocusRef.current = document.activeElement as HTMLElement
// Focus modal modalRef.current?.focus() } else { // Restore focus when closing previousFocusRef.current?.focus() } }, [isOpen])
return isOpen ? ( <div ref={modalRef} role="dialog" aria-modal="true" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()} > {children} </div> ) : null}Remember: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.