Frontend Development Patterns
Modern frontend patterns for React, Next.js, and performant user interfaces.
Component Patterns
Composition Over Inheritance
typescript
1// ✅ GOOD: Component composition
2interface CardProps {
3 children: React.ReactNode
4 variant?: 'default' | 'outlined'
5}
6
7export function Card({ children, variant = 'default' }: CardProps) {
8 return <div className={`card card-${variant}`}>{children}</div>
9}
10
11export function CardHeader({ children }: { children: React.ReactNode }) {
12 return <div className="card-header">{children}</div>
13}
14
15export function CardBody({ children }: { children: React.ReactNode }) {
16 return <div className="card-body">{children}</div>
17}
18
19// Usage
20<Card>
21 <CardHeader>Title</CardHeader>
22 <CardBody>Content</CardBody>
23</Card>
Compound Components
typescript
1interface TabsContextValue {
2 activeTab: string
3 setActiveTab: (tab: string) => void
4}
5
6const TabsContext = createContext<TabsContextValue | undefined>(undefined)
7
8export function Tabs({ children, defaultTab }: {
9 children: React.ReactNode
10 defaultTab: string
11}) {
12 const [activeTab, setActiveTab] = useState(defaultTab)
13
14 return (
15 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
16 {children}
17 </TabsContext.Provider>
18 )
19}
20
21export function TabList({ children }: { children: React.ReactNode }) {
22 return <div className="tab-list">{children}</div>
23}
24
25export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
26 const context = useContext(TabsContext)
27 if (!context) throw new Error('Tab must be used within Tabs')
28
29 return (
30 <button
31 className={context.activeTab === id ? 'active' : ''}
32 onClick={() => context.setActiveTab(id)}
33 >
34 {children}
35 </button>
36 )
37}
38
39// Usage
40<Tabs defaultTab="overview">
41 <TabList>
42 <Tab id="overview">Overview</Tab>
43 <Tab id="details">Details</Tab>
44 </TabList>
45</Tabs>
Render Props Pattern
typescript
1interface DataLoaderProps<T> {
2 url: string
3 children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
4}
5
6export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
7 const [data, setData] = useState<T | null>(null)
8 const [loading, setLoading] = useState(true)
9 const [error, setError] = useState<Error | null>(null)
10
11 useEffect(() => {
12 fetch(url)
13 .then(res => res.json())
14 .then(setData)
15 .catch(setError)
16 .finally(() => setLoading(false))
17 }, [url])
18
19 return <>{children(data, loading, error)}</>
20}
21
22// Usage
23<DataLoader<Market[]> url="/api/markets">
24 {(markets, loading, error) => {
25 if (loading) return <Spinner />
26 if (error) return <Error error={error} />
27 return <MarketList markets={markets!} />
28 }}
29</DataLoader>
Custom Hooks Patterns
State Management Hook
typescript
1export function useToggle(initialValue = false): [boolean, () => void] {
2 const [value, setValue] = useState(initialValue)
3
4 const toggle = useCallback(() => {
5 setValue(v => !v)
6 }, [])
7
8 return [value, toggle]
9}
10
11// Usage
12const [isOpen, toggleOpen] = useToggle()
Async Data Fetching Hook
typescript
1interface UseQueryOptions<T> {
2 onSuccess?: (data: T) => void
3 onError?: (error: Error) => void
4 enabled?: boolean
5}
6
7export function useQuery<T>(
8 key: string,
9 fetcher: () => Promise<T>,
10 options?: UseQueryOptions<T>
11) {
12 const [data, setData] = useState<T | null>(null)
13 const [error, setError] = useState<Error | null>(null)
14 const [loading, setLoading] = useState(false)
15
16 const refetch = useCallback(async () => {
17 setLoading(true)
18 setError(null)
19
20 try {
21 const result = await fetcher()
22 setData(result)
23 options?.onSuccess?.(result)
24 } catch (err) {
25 const error = err as Error
26 setError(error)
27 options?.onError?.(error)
28 } finally {
29 setLoading(false)
30 }
31 }, [fetcher, options])
32
33 useEffect(() => {
34 if (options?.enabled !== false) {
35 refetch()
36 }
37 }, [key, refetch, options?.enabled])
38
39 return { data, error, loading, refetch }
40}
41
42// Usage
43const { data: markets, loading, error, refetch } = useQuery(
44 'markets',
45 () => fetch('/api/markets').then(r => r.json()),
46 {
47 onSuccess: data => console.log('Fetched', data.length, 'markets'),
48 onError: err => console.error('Failed:', err)
49 }
50)
Debounce Hook
typescript
1export function useDebounce<T>(value: T, delay: number): T {
2 const [debouncedValue, setDebouncedValue] = useState<T>(value)
3
4 useEffect(() => {
5 const handler = setTimeout(() => {
6 setDebouncedValue(value)
7 }, delay)
8
9 return () => clearTimeout(handler)
10 }, [value, delay])
11
12 return debouncedValue
13}
14
15// Usage
16const [searchQuery, setSearchQuery] = useState('')
17const debouncedQuery = useDebounce(searchQuery, 500)
18
19useEffect(() => {
20 if (debouncedQuery) {
21 performSearch(debouncedQuery)
22 }
23}, [debouncedQuery])
State Management Patterns
Context + Reducer Pattern
typescript
1interface State {
2 markets: Market[]
3 selectedMarket: Market | null
4 loading: boolean
5}
6
7type Action =
8 | { type: 'SET_MARKETS'; payload: Market[] }
9 | { type: 'SELECT_MARKET'; payload: Market }
10 | { type: 'SET_LOADING'; payload: boolean }
11
12function reducer(state: State, action: Action): State {
13 switch (action.type) {
14 case 'SET_MARKETS':
15 return { ...state, markets: action.payload }
16 case 'SELECT_MARKET':
17 return { ...state, selectedMarket: action.payload }
18 case 'SET_LOADING':
19 return { ...state, loading: action.payload }
20 default:
21 return state
22 }
23}
24
25const MarketContext = createContext<{
26 state: State
27 dispatch: Dispatch<Action>
28} | undefined>(undefined)
29
30export function MarketProvider({ children }: { children: React.ReactNode }) {
31 const [state, dispatch] = useReducer(reducer, {
32 markets: [],
33 selectedMarket: null,
34 loading: false
35 })
36
37 return (
38 <MarketContext.Provider value={{ state, dispatch }}>
39 {children}
40 </MarketContext.Provider>
41 )
42}
43
44export function useMarkets() {
45 const context = useContext(MarketContext)
46 if (!context) throw new Error('useMarkets must be used within MarketProvider')
47 return context
48}
Memoization
typescript
1// ✅ useMemo for expensive computations
2const sortedMarkets = useMemo(() => {
3 return markets.sort((a, b) => b.volume - a.volume)
4}, [markets])
5
6// ✅ useCallback for functions passed to children
7const handleSearch = useCallback((query: string) => {
8 setSearchQuery(query)
9}, [])
10
11// ✅ React.memo for pure components
12export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
13 return (
14 <div className="market-card">
15 <h3>{market.name}</h3>
16 <p>{market.description}</p>
17 </div>
18 )
19})
Code Splitting & Lazy Loading
typescript
1import { lazy, Suspense } from 'react'
2
3// ✅ Lazy load heavy components
4const HeavyChart = lazy(() => import('./HeavyChart'))
5const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
6
7export function Dashboard() {
8 return (
9 <div>
10 <Suspense fallback={<ChartSkeleton />}>
11 <HeavyChart data={data} />
12 </Suspense>
13
14 <Suspense fallback={null}>
15 <ThreeJsBackground />
16 </Suspense>
17 </div>
18 )
19}
Virtualization for Long Lists
typescript
1import { useVirtualizer } from '@tanstack/react-virtual'
2
3export function VirtualMarketList({ markets }: { markets: Market[] }) {
4 const parentRef = useRef<HTMLDivElement>(null)
5
6 const virtualizer = useVirtualizer({
7 count: markets.length,
8 getScrollElement: () => parentRef.current,
9 estimateSize: () => 100, // Estimated row height
10 overscan: 5 // Extra items to render
11 })
12
13 return (
14 <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
15 <div
16 style={{
17 height: `${virtualizer.getTotalSize()}px`,
18 position: 'relative'
19 }}
20 >
21 {virtualizer.getVirtualItems().map(virtualRow => (
22 <div
23 key={virtualRow.index}
24 style={{
25 position: 'absolute',
26 top: 0,
27 left: 0,
28 width: '100%',
29 height: `${virtualRow.size}px`,
30 transform: `translateY(${virtualRow.start}px)`
31 }}
32 >
33 <MarketCard market={markets[virtualRow.index]} />
34 </div>
35 ))}
36 </div>
37 </div>
38 )
39}
typescript
1interface FormData {
2 name: string
3 description: string
4 endDate: string
5}
6
7interface FormErrors {
8 name?: string
9 description?: string
10 endDate?: string
11}
12
13export function CreateMarketForm() {
14 const [formData, setFormData] = useState<FormData>({
15 name: '',
16 description: '',
17 endDate: ''
18 })
19
20 const [errors, setErrors] = useState<FormErrors>({})
21
22 const validate = (): boolean => {
23 const newErrors: FormErrors = {}
24
25 if (!formData.name.trim()) {
26 newErrors.name = 'Name is required'
27 } else if (formData.name.length > 200) {
28 newErrors.name = 'Name must be under 200 characters'
29 }
30
31 if (!formData.description.trim()) {
32 newErrors.description = 'Description is required'
33 }
34
35 if (!formData.endDate) {
36 newErrors.endDate = 'End date is required'
37 }
38
39 setErrors(newErrors)
40 return Object.keys(newErrors).length === 0
41 }
42
43 const handleSubmit = async (e: React.FormEvent) => {
44 e.preventDefault()
45
46 if (!validate()) return
47
48 try {
49 await createMarket(formData)
50 // Success handling
51 } catch (error) {
52 // Error handling
53 }
54 }
55
56 return (
57 <form onSubmit={handleSubmit}>
58 <input
59 value={formData.name}
60 onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
61 placeholder="Market name"
62 />
63 {errors.name && <span className="error">{errors.name}</span>}
64
65 {/* Other fields */}
66
67 <button type="submit">Create Market</button>
68 </form>
69 )
70}
Error Boundary Pattern
typescript
1interface ErrorBoundaryState {
2 hasError: boolean
3 error: Error | null
4}
5
6export class ErrorBoundary extends React.Component<
7 { children: React.ReactNode },
8 ErrorBoundaryState
9> {
10 state: ErrorBoundaryState = {
11 hasError: false,
12 error: null
13 }
14
15 static getDerivedStateFromError(error: Error): ErrorBoundaryState {
16 return { hasError: true, error }
17 }
18
19 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
20 console.error('Error boundary caught:', error, errorInfo)
21 }
22
23 render() {
24 if (this.state.hasError) {
25 return (
26 <div className="error-fallback">
27 <h2>Something went wrong</h2>
28 <p>{this.state.error?.message}</p>
29 <button onClick={() => this.setState({ hasError: false })}>
30 Try again
31 </button>
32 </div>
33 )
34 }
35
36 return this.props.children
37 }
38}
39
40// Usage
41<ErrorBoundary>
42 <App />
43</ErrorBoundary>
Animation Patterns
Framer Motion Animations
typescript
1import { motion, AnimatePresence } from 'framer-motion'
2
3// ✅ List animations
4export function AnimatedMarketList({ markets }: { markets: Market[] }) {
5 return (
6 <AnimatePresence>
7 {markets.map(market => (
8 <motion.div
9 key={market.id}
10 initial={{ opacity: 0, y: 20 }}
11 animate={{ opacity: 1, y: 0 }}
12 exit={{ opacity: 0, y: -20 }}
13 transition={{ duration: 0.3 }}
14 >
15 <MarketCard market={market} />
16 </motion.div>
17 ))}
18 </AnimatePresence>
19 )
20}
21
22// ✅ Modal animations
23export function Modal({ isOpen, onClose, children }: ModalProps) {
24 return (
25 <AnimatePresence>
26 {isOpen && (
27 <>
28 <motion.div
29 className="modal-overlay"
30 initial={{ opacity: 0 }}
31 animate={{ opacity: 1 }}
32 exit={{ opacity: 0 }}
33 onClick={onClose}
34 />
35 <motion.div
36 className="modal-content"
37 initial={{ opacity: 0, scale: 0.9, y: 20 }}
38 animate={{ opacity: 1, scale: 1, y: 0 }}
39 exit={{ opacity: 0, scale: 0.9, y: 20 }}
40 >
41 {children}
42 </motion.div>
43 </>
44 )}
45 </AnimatePresence>
46 )
47}
Accessibility Patterns
Keyboard Navigation
typescript
1export function Dropdown({ options, onSelect }: DropdownProps) {
2 const [isOpen, setIsOpen] = useState(false)
3 const [activeIndex, setActiveIndex] = useState(0)
4
5 const handleKeyDown = (e: React.KeyboardEvent) => {
6 switch (e.key) {
7 case 'ArrowDown':
8 e.preventDefault()
9 setActiveIndex(i => Math.min(i + 1, options.length - 1))
10 break
11 case 'ArrowUp':
12 e.preventDefault()
13 setActiveIndex(i => Math.max(i - 1, 0))
14 break
15 case 'Enter':
16 e.preventDefault()
17 onSelect(options[activeIndex])
18 setIsOpen(false)
19 break
20 case 'Escape':
21 setIsOpen(false)
22 break
23 }
24 }
25
26 return (
27 <div
28 role="combobox"
29 aria-expanded={isOpen}
30 aria-haspopup="listbox"
31 onKeyDown={handleKeyDown}
32 >
33 {/* Dropdown implementation */}
34 </div>
35 )
36}
Focus Management
typescript
1export function Modal({ isOpen, onClose, children }: ModalProps) {
2 const modalRef = useRef<HTMLDivElement>(null)
3 const previousFocusRef = useRef<HTMLElement | null>(null)
4
5 useEffect(() => {
6 if (isOpen) {
7 // Save currently focused element
8 previousFocusRef.current = document.activeElement as HTMLElement
9
10 // Focus modal
11 modalRef.current?.focus()
12 } else {
13 // Restore focus when closing
14 previousFocusRef.current?.focus()
15 }
16 }, [isOpen])
17
18 return isOpen ? (
19 <div
20 ref={modalRef}
21 role="dialog"
22 aria-modal="true"
23 tabIndex={-1}
24 onKeyDown={e => e.key === 'Escape' && onClose()}
25 >
26 {children}
27 </div>
28 ) : null
29}
Remember: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.