8# Frontend Development Patterns
9
10Modern frontend patterns for React, Next.js, and performant user interfaces.
11
12## When to Activate
13
14- Building React components (composition, props, rendering)
15- Managing state (useState, useReducer, Zustand, Context)
16- Implementing data fetching (SWR, React Query, server components)
17- Optimizing performance (memoization, virtualization, code splitting)
18- Working with forms (validation, controlled inputs, Zod schemas)
19- Handling client-side routing and navigation
20- Building accessible, responsive UI patterns
21
22## Component Patterns
23
24### Composition Over Inheritance
25
26```typescript
27// PASS: GOOD: Component composition
28interface CardProps {
29 children: React.ReactNode
30 variant?: 'default' | 'outlined'
31}
32
33export function Card({ children, variant = 'default' }: CardProps) {
34 return <div className={card card-${variant}}>{children}</div>
35}
36
37export function CardHeader({ children }: { children: React.ReactNode }) {
38 return <div className="card-header">{children}</div>
39}
40
41export function CardBody({ children }: { children: React.ReactNode }) {
42 return <div className="card-body">{children}</div>
43}
44
45// Usage
46<Card>
47 <CardHeader>Title</CardHeader>
48 <CardBody>Content</CardBody>
49</Card>
50```
51
52### Compound Components
53
54```typescript
55interface TabsContextValue {
56 activeTab: string
57 setActiveTab: (tab: string) => void
58}
59
60const TabsContext = createContext<TabsContextValue | undefined>(undefined)
61
62export function Tabs({ children, defaultTab }: {
63 children: React.ReactNode
64 defaultTab: string
65}) {
66 const [activeTab, setActiveTab] = useState(defaultTab)
67
68 return (
69 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
70 {children}
71 </TabsContext.Provider>
72 )
73}
74
75export function TabList({ children }: { children: React.ReactNode }) {
76 return <div className="tab-list">{children}</div>
77}
78
79export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
80 const context = useContext(TabsContext)
81 if (!context) throw new Error('Tab must be used within Tabs')
82
83 return (
84 <button
85 className={context.activeTab === id ? 'active' : ''}
86 onClick={() => context.setActiveTab(id)}
87 >
88 {children}
89 </button>
90 )
91}
92
93// Usage
94<Tabs defaultTab="overview">
95 <TabList>
96 <Tab id="overview">Overview</Tab>
97 <Tab id="details">Details</Tab>
98 </TabList>
99</Tabs>
100```
101
102### Render Props Pattern
103
104```typescript
105interface DataLoaderProps<T> {
106 url: string
107 children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
108}
109
110export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
111 const [data, setData] = useState<T | null>(null)
112 const [loading, setLoading] = useState(true)
113 const [error, setError] = useState<Error | null>(null)
114
115 useEffect(() => {
116 fetch(url)
117 .then(res => res.json())
118 .then(setData)
119 .catch(setError)
120 .finally(() => setLoading(false))
121 }, [url])
122
123 return <>{children(data, loading, error)}</>
124}
125
126// Usage
127<DataLoader<Market[]> url="/api/markets">
128 {(markets, loading, error) => {
129 if (loading) return <Spinner />
130 if (error) return <Error error={error} />
131 return <MarketList markets={markets!} />
132 }}
133</DataLoader>
134```
135
136## Custom Hooks Patterns
137
138### State Management Hook
139
140```typescript
141export function useToggle(initialValue = false): [boolean, () => void] {
142 const [value, setValue] = useState(initialValue)
143
144 const toggle = useCallback(() => {
145 setValue(v => !v)
146 }, [])
147
148 return [value, toggle]
149}
150
151// Usage
152const [isOpen, toggleOpen] = useToggle()
153```
154
155### Async Data Fetching Hook
156
157```typescript
158interface UseQueryOptions<T> {
159 onSuccess?: (data: T) => void
160 onError?: (error: Error) => void
161 enabled?: boolean
162}
163
164export function useQuery<T>(
165 key: string,
166 fetcher: () => Promise<T>,
167 options?: UseQueryOptions<T>
168) {
169 const [data, setData] = useState<T | null>(null)
170 const [error, setError] = useState<Error | null>(null)
171 const [loading, setLoading] = useState(false)
172
173 // Keep the latest fetcher/options in refs so refetch stays referentially
174 // stable even when callers pass inline functions and object literals.
175 // Without this, every render creates a new refetch, and the effect below
176 // re-runs after each state update - an infinite fetch loop.
177 const fetcherRef = useRef(fetcher)
178 const optionsRef = useRef(options)
179 useEffect(() => {
180 fetcherRef.current = fetcher
181 optionsRef.current = options
182 })
183
184 const refetch = useCallback(async () => {
185 setLoading(true)
186 setError(null)
187
188 try {
189 const result = await fetcherRef.current()
190 setData(result)
191 optionsRef.current?.onSuccess?.(result)
192 } catch (err) {
193 const error = err as Error
194 setError(error)
195 optionsRef.current?.onError?.(error)
196 } finally {
197 setLoading(false)
198 }
199 }, [])
200
201 const enabled = options?.enabled !== false
202
203 useEffect(() => {
204 if (enabled) {
205 refetch()
206 }
207 }, [key, enabled, refetch])
208
209 return { data, error, loading, refetch }
210}
211
212// Usage
213const { data: markets, loading, error, refetch } = useQuery(
214 'markets',
215 () => fetch('/api/markets').then(r => r.json()),
216 {
217 onSuccess: data => console.log('Fetched', data.length, 'markets'),
218 onError: err => console.error('Failed:', err)
219 }
220)
221```
222
223### Debounce Hook
224
225```typescript
226export function useDebounce<T>(value: T, delay: number): T {
227 const [debouncedValue, setDebouncedValue] = useState<T>(value)
228
229 useEffect(() => {
230 const handler = setTimeout(() => {
231 setDebouncedValue(value)
232 }, delay)
233
234 return () => clearTimeout(handler)
235 }, [value, delay])
236
237 return debouncedValue
238}
239
240// Usage
241const [searchQuery, setSearchQuery] = useState('')
242const debouncedQuery = useDebounce(searchQuery, 500)
243
244useEffect(() => {
245 if (debouncedQuery) {
246 performSearch(debouncedQuery)
247 }
248}, [debouncedQuery])
249```
250
251## State Management Patterns
252
253### Context + Reducer Pattern
254
255```typescript
256interface State {
257 markets: Market[]
258 selectedMarket: Market | null
259 loading: boolean
260}
261
262type Action =
263 | { type: 'SET_MARKETS'; payload: Market[] }
264 | { type: 'SELECT_MARKET'; payload: Market }
265 | { type: 'SET_LOADING'; payload: boolean }
266
267function reducer(state: State, action: Action): State {
268 switch (action.type) {
269 case 'SET_MARKETS':
270 return { ...state, markets: action.payload }
271 case 'SELECT_MARKET':
272 return { ...state, selectedMarket: action.payload }
273 case 'SET_LOADING':
274 return { ...state, loading: action.payload }
275 default:
276 return state
277 }
278}
279
280const MarketContext = createContext<{
281 state: State
282 dispatch: Dispatch<Action>
283} | undefined>(undefined)
284
285export function MarketProvider({ children }: { children: React.ReactNode }) {
286 const [state, dispatch] = useReducer(reducer, {
287 markets: [],
288 selectedMarket: null,
289 loading: false
290 })
291
292 return (
293 <MarketContext.Provider value={{ state, dispatch }}>
294 {children}
295 </MarketContext.Provider>
296 )
297}
298
299export function useMarkets() {
300 const context = useContext(MarketContext)
301 if (!context) throw new Error('useMarkets must be used within MarketProvider')
302 return context
303}
304```
305
306## Performance Optimization
307
308### Memoization
309
310```typescript
311// PASS: useMemo for expensive computations
312// Copy before sorting - Array.prototype.sort mutates in place
313const sortedMarkets = useMemo(() => {
314 return [...markets].sort((a, b) => b.volume - a.volume)
315}, [markets])
316
317// PASS: useCallback for functions passed to children
318const handleSearch = useCallback((query: string) => {
319 setSearchQuery(query)
320}, [])
321
322// PASS: React.memo for pure components
323export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
324 return (
325 <div className="market-card">
326 <h3>{market.name}</h3>
327 <p>{market.description}</p>
328 </div>
329 )
330})
331```
332
333### Code Splitting & Lazy Loading
334
335```typescript
336import { lazy, Suspense } from 'react'
337
338// PASS: Lazy load heavy components
339const HeavyChart = lazy(() => import('./HeavyChart'))
340const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
341
342export function Dashboard() {
343 return (
344 <div>
345 <Suspense fallback={<ChartSkeleton />}>
346 <HeavyChart data={data} />
347 </Suspense>
348
349 <Suspense fallback={null}>
350 <ThreeJsBackground />
351 </Suspense>
352 </div>
353 )
354}
355```
356
357### Virtualization for Long Lists
358
359```typescript
360import { useVirtualizer } from '@tanstack/react-virtual'
361
362export function VirtualMarketList({ markets }: { markets: Market[] }) {
363 const parentRef = useRef<HTMLDivElement>(null)
364
365 const virtualizer = useVirtualizer({
366 count: markets.length,
367 getScrollElement: () => parentRef.current,
368 estimateSize: () => 100, // Estimated row height
369 overscan: 5 // Extra items to render
370 })
371
372 return (
373 <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
374 <div
375 style={{
376 height: ${virtualizer.getTotalSize()}px,
377 position: 'relative'
378 }}
379 >
380 {virtualizer.getVirtualItems().map(virtualRow => (
381 <div
382 key={virtualRow.index}
383 style={{
384 position: 'absolute',
385 top: 0,
386 left: 0,
387 width: '100%',
388 height: ${virtualRow.size}px,
389 transform: translateY(${virtualRow.start}px)
390 }}
391 >
392 <MarketCard market={markets[virtualRow.index]} />
393 </div>
394 ))}
395 </div>
396 </div>
397 )
398}
399```
400
401## Form Handling Patterns
402
403### Controlled Form with Validation
404
405```typescript
406interface FormData {
407 name: string
408 description: string
409 endDate: string
410}
411
412interface FormErrors {
413 name?: string
414 description?: string
415 endDate?: string
416}
417
418export function CreateMarketForm() {
419 const [formData, setFormData] = useState<FormData>({
420 name: '',
421 description: '',
422 endDate: ''
423 })
424
425 const [errors, setErrors] = useState<FormErrors>({})
426
427 const validate = (): boolean => {
428 const newErrors: FormErrors = {}
429
430 if (!formData.name.trim()) {
431 newErrors.name = 'Name is required'
432 } else if (formData.name.length > 200) {
433 newErrors.name = 'Name must be under 200 characters'
434 }
435
436 if (!formData.description.trim()) {
437 newErrors.description = 'Description is required'
438 }
439
440 if (!formData.endDate) {
441 newErrors.endDate = 'End date is required'
442 }
443
444 setErrors(newErrors)
445 return Object.keys(newErrors).length === 0
446 }
447
448 const handleSubmit = async (e: React.FormEvent) => {
449 e.preventDefault()
450
451 if (!validate()) return
452
453 try {
454 await createMarket(formData)
455 // Success handling
456 } catch (error) {
457 // Error handling
458 }
459 }
460
461 return (
462 <form onSubmit={handleSubmit}>
463 <input
464 value={formData.name}
465 onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
466 placeholder="Market name"
467 />
468 {errors.name && <span className="error">{errors.name}</span>}
469
470 {/* Other fields */}
471
472 <button type="submit">Create Market</button>
473 </form>
474 )
475}
476```
477
478## Error Boundary Pattern
479
480```typescript
481interface ErrorBoundaryState {
482 hasError: boolean
483 error: Error | null
484}
485
486export class ErrorBoundary extends React.Component<
487 { children: React.ReactNode },
488 ErrorBoundaryState
489> {
490 state: ErrorBoundaryState = {
491 hasError: false,
492 error: null
493 }
494
495 static getDerivedStateFromError(error: Error): ErrorBoundaryState {
496 return { hasError: true, error }
497 }
498
499 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
500 console.error('Error boundary caught:', error, errorInfo)
501 }
502
503 render() {
504 if (this.state.hasError) {
505 return (
506 <div className="error-fallback">
507 <h2>Something went wrong</h2>
508 <p>{this.state.error?.message}</p>
509 <button onClick={() => this.setState({ hasError: false })}>
510 Try again
511 </button>
512 </div>
513 )
514 }
515
516 return this.props.children
517 }
518}
519
520// Usage
521<ErrorBoundary>
522 <App />
523</ErrorBoundary>
524```
525
526## Animation Patterns
527
528### Framer Motion Animations
529
530```typescript
531import { motion, AnimatePresence } from 'framer-motion'
532
533// PASS: List animations
534export function AnimatedMarketList({ markets }: { markets: Market[] }) {
535 return (
536 <AnimatePresence>
537 {markets.map(market => (
538 <motion.div
539 key={market.id}
540 initial={{ opacity: 0, y: 20 }}
541 animate={{ opacity: 1, y: 0 }}
542 exit={{ opacity: 0, y: -20 }}
543 transition={{ duration: 0.3 }}
544 >
545 <MarketCard market={market} />
546 </motion.div>
547 ))}
548 </AnimatePresence>
549 )
550}
551
552// PASS: Modal animations
553export function Modal({ isOpen, onClose, children }: ModalProps) {
554 return (
555 <AnimatePresence>
556 {isOpen && (
557 <>
558 <motion.div
559 className="modal-overlay"
560 initial={{ opacity: 0 }}
561 animate={{ opacity: 1 }}
562 exit={{ opacity: 0 }}
563 onClick={onClose}
564 />
565 <motion.div
566 className="modal-content"
567 initial={{ opacity: 0, scale: 0.9, y: 20 }}
568 animate={{ opacity: 1, scale: 1, y: 0 }}
569 exit={{ opacity: 0, scale: 0.9, y: 20 }}
570 >
571 {children}
572 </motion.div>
573 </>
574 )}
575 </AnimatePresence>
576 )
577}
578```
579
580## Accessibility Patterns
581
582### Keyboard Navigation
583
584```typescript
585export function Dropdown({ options, onSelect }: DropdownProps) {
586 const [isOpen, setIsOpen] = useState(false)
587 const [activeIndex, setActiveIndex] = useState(0)
588
589 const handleKeyDown = (e: React.KeyboardEvent) => {
590 switch (e.key) {
591 case 'ArrowDown':
592 e.preventDefault()
593 setActiveIndex(i => Math.min(i + 1, options.length - 1))
594 break
595 case 'ArrowUp':
596 e.preventDefault()
597 setActiveIndex(i => Math.max(i - 1, 0))
598 break
599 case 'Enter':
600 e.preventDefault()
601 onSelect(options[activeIndex])
602 setIsOpen(false)
603 break
604 case 'Escape':
605 setIsOpen(false)
606 break
607 }
608 }
609
610 return (
611 <div
612 role="combobox"
613 aria-expanded={isOpen}
614 aria-haspopup="listbox"
615 onKeyDown={handleKeyDown}
616 >
617 {/* Dropdown implementation */}
618 </div>
619 )
620}
621```
622
623### Focus Management
624
625```typescript
626export function Modal({ isOpen, onClose, children }: ModalProps) {
627 const modalRef = useRef<HTMLDivElement>(null)
628 const previousFocusRef = useRef<HTMLElement | null>(null)
629
630 useEffect(() => {
631 if (isOpen) {
632 // Save currently focused element
633 previousFocusRef.current = document.activeElement as HTMLElement
634
635 // Focus modal
636 modalRef.current?.focus()
637 } else {
638 // Restore focus when closing
639 previousFocusRef.current?.focus()
640 }
641 }, [isOpen])
642
643 return isOpen ? (
644 <div
645 ref={modalRef}
646 role="dialog"
647 aria-modal="true"
648 tabIndex={-1}
649 onKeyDown={e => e.key === 'Escape' && onClose()}
650 >
651 {children}
652 </div>
653 ) : null
654}
655```
656
657**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.
658