practical-patterns.tsx raw
1 # React Practical Examples
2
3 This file contains real-world examples of React patterns and solutions.
4
5 ## Example 1: Custom Hook for Data Fetching
6
7 ```typescript
8 import { useState, useEffect } from 'react'
9
10 interface FetchState<T> {
11 data: T | null
12 loading: boolean
13 error: Error | null
14 }
15
16 const useFetch = <T,>(url: string) => {
17 const [state, setState] = useState<FetchState<T>>({
18 data: null,
19 loading: true,
20 error: null
21 })
22
23 useEffect(() => {
24 let cancelled = false
25 const controller = new AbortController()
26
27 const fetchData = async () => {
28 try {
29 setState(prev => ({ ...prev, loading: true, error: null }))
30
31 const response = await fetch(url, {
32 signal: controller.signal
33 })
34
35 if (!response.ok) {
36 throw new Error(`HTTP error! status: ${response.status}`)
37 }
38
39 const data = await response.json()
40
41 if (!cancelled) {
42 setState({ data, loading: false, error: null })
43 }
44 } catch (error) {
45 if (!cancelled && error.name !== 'AbortError') {
46 setState({
47 data: null,
48 loading: false,
49 error: error as Error
50 })
51 }
52 }
53 }
54
55 fetchData()
56
57 return () => {
58 cancelled = true
59 controller.abort()
60 }
61 }, [url])
62
63 return state
64 }
65
66 // Usage
67 const UserProfile = ({ userId }: { userId: string }) => {
68 const { data, loading, error } = useFetch<User>(`/api/users/${userId}`)
69
70 if (loading) return <Spinner />
71 if (error) return <ErrorMessage error={error} />
72 if (!data) return null
73
74 return <UserCard user={data} />
75 }
76 ```
77
78 ## Example 2: Form with Validation
79
80 ```typescript
81 import { useState, useCallback } from 'react'
82 import { z } from 'zod'
83
84 const userSchema = z.object({
85 name: z.string().min(2, 'Name must be at least 2 characters'),
86 email: z.string().email('Invalid email address'),
87 age: z.number().min(18, 'Must be 18 or older')
88 })
89
90 type UserForm = z.infer<typeof userSchema>
91 type FormErrors = Partial<Record<keyof UserForm, string>>
92
93 const UserForm = () => {
94 const [formData, setFormData] = useState<UserForm>({
95 name: '',
96 email: '',
97 age: 0
98 })
99 const [errors, setErrors] = useState<FormErrors>({})
100 const [isSubmitting, setIsSubmitting] = useState(false)
101
102 const handleChange = useCallback((
103 field: keyof UserForm,
104 value: string | number
105 ) => {
106 setFormData(prev => ({ ...prev, [field]: value }))
107 // Clear error when user starts typing
108 setErrors(prev => ({ ...prev, [field]: undefined }))
109 }, [])
110
111 const handleSubmit = async (e: React.FormEvent) => {
112 e.preventDefault()
113
114 // Validate
115 const result = userSchema.safeParse(formData)
116 if (!result.success) {
117 const fieldErrors: FormErrors = {}
118 result.error.errors.forEach(err => {
119 const field = err.path[0] as keyof UserForm
120 fieldErrors[field] = err.message
121 })
122 setErrors(fieldErrors)
123 return
124 }
125
126 // Submit
127 setIsSubmitting(true)
128 try {
129 await submitUser(result.data)
130 // Success handling
131 } catch (error) {
132 console.error(error)
133 } finally {
134 setIsSubmitting(false)
135 }
136 }
137
138 return (
139 <form onSubmit={handleSubmit}>
140 <div>
141 <label htmlFor="name">Name</label>
142 <input
143 id="name"
144 value={formData.name}
145 onChange={e => handleChange('name', e.target.value)}
146 />
147 {errors.name && <span className="error">{errors.name}</span>}
148 </div>
149
150 <div>
151 <label htmlFor="email">Email</label>
152 <input
153 id="email"
154 type="email"
155 value={formData.email}
156 onChange={e => handleChange('email', e.target.value)}
157 />
158 {errors.email && <span className="error">{errors.email}</span>}
159 </div>
160
161 <div>
162 <label htmlFor="age">Age</label>
163 <input
164 id="age"
165 type="number"
166 value={formData.age || ''}
167 onChange={e => handleChange('age', Number(e.target.value))}
168 />
169 {errors.age && <span className="error">{errors.age}</span>}
170 </div>
171
172 <button type="submit" disabled={isSubmitting}>
173 {isSubmitting ? 'Submitting...' : 'Submit'}
174 </button>
175 </form>
176 )
177 }
178 ```
179
180 ## Example 3: Modal with Portal
181
182 ```typescript
183 import { createPortal } from 'react-dom'
184 import { useEffect, useRef, useState } from 'react'
185
186 interface ModalProps {
187 isOpen: boolean
188 onClose: () => void
189 children: React.ReactNode
190 title?: string
191 }
192
193 const Modal = ({ isOpen, onClose, children, title }: ModalProps) => {
194 const modalRef = useRef<HTMLDivElement>(null)
195
196 // Close on Escape key
197 useEffect(() => {
198 const handleEscape = (e: KeyboardEvent) => {
199 if (e.key === 'Escape') onClose()
200 }
201
202 if (isOpen) {
203 document.addEventListener('keydown', handleEscape)
204 // Prevent body scroll
205 document.body.style.overflow = 'hidden'
206 }
207
208 return () => {
209 document.removeEventListener('keydown', handleEscape)
210 document.body.style.overflow = 'unset'
211 }
212 }, [isOpen, onClose])
213
214 // Close on backdrop click
215 const handleBackdropClick = (e: React.MouseEvent) => {
216 if (e.target === modalRef.current) {
217 onClose()
218 }
219 }
220
221 if (!isOpen) return null
222
223 return createPortal(
224 <div
225 ref={modalRef}
226 className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
227 onClick={handleBackdropClick}
228 >
229 <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
230 <div className="flex justify-between items-center mb-4">
231 {title && <h2 className="text-xl font-bold">{title}</h2>}
232 <button
233 onClick={onClose}
234 className="text-gray-500 hover:text-gray-700"
235 aria-label="Close modal"
236 >
237 ✕
238 </button>
239 </div>
240 {children}
241 </div>
242 </div>,
243 document.body
244 )
245 }
246
247 // Usage
248 const App = () => {
249 const [isOpen, setIsOpen] = useState(false)
250
251 return (
252 <>
253 <button onClick={() => setIsOpen(true)}>Open Modal</button>
254 <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="My Modal">
255 <p>Modal content goes here</p>
256 <button onClick={() => setIsOpen(false)}>Close</button>
257 </Modal>
258 </>
259 )
260 }
261 ```
262
263 ## Example 4: Infinite Scroll
264
265 ```typescript
266 import { useState, useEffect, useRef, useCallback } from 'react'
267
268 interface InfiniteScrollProps<T> {
269 fetchData: (page: number) => Promise<T[]>
270 renderItem: (item: T, index: number) => React.ReactNode
271 loader?: React.ReactNode
272 endMessage?: React.ReactNode
273 }
274
275 const InfiniteScroll = <T extends { id: string | number },>({
276 fetchData,
277 renderItem,
278 loader = <div>Loading...</div>,
279 endMessage = <div>No more items</div>
280 }: InfiniteScrollProps<T>) => {
281 const [items, setItems] = useState<T[]>([])
282 const [page, setPage] = useState(1)
283 const [loading, setLoading] = useState(false)
284 const [hasMore, setHasMore] = useState(true)
285 const observerRef = useRef<IntersectionObserver | null>(null)
286 const loadMoreRef = useRef<HTMLDivElement>(null)
287
288 const loadMore = useCallback(async () => {
289 if (loading || !hasMore) return
290
291 setLoading(true)
292 try {
293 const newItems = await fetchData(page)
294
295 if (newItems.length === 0) {
296 setHasMore(false)
297 } else {
298 setItems(prev => [...prev, ...newItems])
299 setPage(prev => prev + 1)
300 }
301 } catch (error) {
302 console.error('Failed to load items:', error)
303 } finally {
304 setLoading(false)
305 }
306 }, [page, loading, hasMore, fetchData])
307
308 // Set up intersection observer
309 useEffect(() => {
310 observerRef.current = new IntersectionObserver(
311 entries => {
312 if (entries[0].isIntersecting) {
313 loadMore()
314 }
315 },
316 { threshold: 0.1 }
317 )
318
319 const currentRef = loadMoreRef.current
320 if (currentRef) {
321 observerRef.current.observe(currentRef)
322 }
323
324 return () => {
325 if (observerRef.current && currentRef) {
326 observerRef.current.unobserve(currentRef)
327 }
328 }
329 }, [loadMore])
330
331 // Initial load
332 useEffect(() => {
333 loadMore()
334 }, [])
335
336 return (
337 <div>
338 {items.map((item, index) => (
339 <div key={item.id}>
340 {renderItem(item, index)}
341 </div>
342 ))}
343
344 <div ref={loadMoreRef}>
345 {loading && loader}
346 {!loading && !hasMore && endMessage}
347 </div>
348 </div>
349 )
350 }
351
352 // Usage
353 const PostsList = () => {
354 const fetchPosts = async (page: number) => {
355 const response = await fetch(`/api/posts?page=${page}`)
356 return response.json()
357 }
358
359 return (
360 <InfiniteScroll<Post>
361 fetchData={fetchPosts}
362 renderItem={(post) => <PostCard post={post} />}
363 />
364 )
365 }
366 ```
367
368 ## Example 5: Dark Mode Toggle
369
370 ```typescript
371 import { createContext, useContext, useState, useEffect } from 'react'
372
373 type Theme = 'light' | 'dark'
374
375 interface ThemeContextType {
376 theme: Theme
377 toggleTheme: () => void
378 }
379
380 const ThemeContext = createContext<ThemeContextType | null>(null)
381
382 export const useTheme = () => {
383 const context = useContext(ThemeContext)
384 if (!context) {
385 throw new Error('useTheme must be used within ThemeProvider')
386 }
387 return context
388 }
389
390 export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
391 const [theme, setTheme] = useState<Theme>(() => {
392 // Check localStorage and system preference
393 const saved = localStorage.getItem('theme') as Theme | null
394 if (saved) return saved
395
396 if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
397 return 'dark'
398 }
399
400 return 'light'
401 })
402
403 useEffect(() => {
404 // Update DOM and localStorage
405 const root = document.documentElement
406 root.classList.remove('light', 'dark')
407 root.classList.add(theme)
408 localStorage.setItem('theme', theme)
409 }, [theme])
410
411 const toggleTheme = () => {
412 setTheme(prev => prev === 'light' ? 'dark' : 'light')
413 }
414
415 return (
416 <ThemeContext.Provider value={{ theme, toggleTheme }}>
417 {children}
418 </ThemeContext.Provider>
419 )
420 }
421
422 // Usage
423 const ThemeToggle = () => {
424 const { theme, toggleTheme } = useTheme()
425
426 return (
427 <button onClick={toggleTheme} aria-label="Toggle theme">
428 {theme === 'light' ? '🌙' : '☀️'}
429 </button>
430 )
431 }
432 ```
433
434 ## Example 6: Debounced Search
435
436 ```typescript
437 import { useState, useEffect, useMemo } from 'react'
438
439 const useDebounce = <T,>(value: T, delay: number): T => {
440 const [debouncedValue, setDebouncedValue] = useState(value)
441
442 useEffect(() => {
443 const timer = setTimeout(() => {
444 setDebouncedValue(value)
445 }, delay)
446
447 return () => {
448 clearTimeout(timer)
449 }
450 }, [value, delay])
451
452 return debouncedValue
453 }
454
455 const SearchPage = () => {
456 const [query, setQuery] = useState('')
457 const [results, setResults] = useState<Product[]>([])
458 const [loading, setLoading] = useState(false)
459
460 const debouncedQuery = useDebounce(query, 500)
461
462 useEffect(() => {
463 if (!debouncedQuery) {
464 setResults([])
465 return
466 }
467
468 const searchProducts = async () => {
469 setLoading(true)
470 try {
471 const response = await fetch(`/api/search?q=${debouncedQuery}`)
472 const data = await response.json()
473 setResults(data)
474 } catch (error) {
475 console.error('Search failed:', error)
476 } finally {
477 setLoading(false)
478 }
479 }
480
481 searchProducts()
482 }, [debouncedQuery])
483
484 return (
485 <div>
486 <input
487 type="search"
488 value={query}
489 onChange={e => setQuery(e.target.value)}
490 placeholder="Search products..."
491 />
492
493 {loading && <Spinner />}
494
495 {!loading && results.length > 0 && (
496 <div>
497 {results.map(product => (
498 <ProductCard key={product.id} product={product} />
499 ))}
500 </div>
501 )}
502
503 {!loading && query && results.length === 0 && (
504 <p>No results found for "{query}"</p>
505 )}
506 </div>
507 )
508 }
509 ```
510
511 ## Example 7: Tabs Component
512
513 ```typescript
514 import { createContext, useContext, useState, useId } from 'react'
515
516 interface TabsContextType {
517 activeTab: string
518 setActiveTab: (id: string) => void
519 tabsId: string
520 }
521
522 const TabsContext = createContext<TabsContextType | null>(null)
523
524 const useTabs = () => {
525 const context = useContext(TabsContext)
526 if (!context) throw new Error('Tabs compound components must be used within Tabs')
527 return context
528 }
529
530 interface TabsProps {
531 children: React.ReactNode
532 defaultValue: string
533 className?: string
534 }
535
536 const Tabs = ({ children, defaultValue, className }: TabsProps) => {
537 const [activeTab, setActiveTab] = useState(defaultValue)
538 const tabsId = useId()
539
540 return (
541 <TabsContext.Provider value={{ activeTab, setActiveTab, tabsId }}>
542 <div className={className}>
543 {children}
544 </div>
545 </TabsContext.Provider>
546 )
547 }
548
549 const TabsList = ({ children, className }: {
550 children: React.ReactNode
551 className?: string
552 }) => (
553 <div role="tablist" className={className}>
554 {children}
555 </div>
556 )
557
558 interface TabsTriggerProps {
559 value: string
560 children: React.ReactNode
561 className?: string
562 }
563
564 const TabsTrigger = ({ value, children, className }: TabsTriggerProps) => {
565 const { activeTab, setActiveTab, tabsId } = useTabs()
566 const isActive = activeTab === value
567
568 return (
569 <button
570 role="tab"
571 id={`${tabsId}-tab-${value}`}
572 aria-controls={`${tabsId}-panel-${value}`}
573 aria-selected={isActive}
574 onClick={() => setActiveTab(value)}
575 className={`${className} ${isActive ? 'active' : ''}`}
576 >
577 {children}
578 </button>
579 )
580 }
581
582 interface TabsContentProps {
583 value: string
584 children: React.ReactNode
585 className?: string
586 }
587
588 const TabsContent = ({ value, children, className }: TabsContentProps) => {
589 const { activeTab, tabsId } = useTabs()
590
591 if (activeTab !== value) return null
592
593 return (
594 <div
595 role="tabpanel"
596 id={`${tabsId}-panel-${value}`}
597 aria-labelledby={`${tabsId}-tab-${value}`}
598 className={className}
599 >
600 {children}
601 </div>
602 )
603 }
604
605 // Export compound component
606 export { Tabs, TabsList, TabsTrigger, TabsContent }
607
608 // Usage
609 const App = () => (
610 <Tabs defaultValue="profile">
611 <TabsList>
612 <TabsTrigger value="profile">Profile</TabsTrigger>
613 <TabsTrigger value="settings">Settings</TabsTrigger>
614 <TabsTrigger value="notifications">Notifications</TabsTrigger>
615 </TabsList>
616
617 <TabsContent value="profile">
618 <h2>Profile Content</h2>
619 </TabsContent>
620
621 <TabsContent value="settings">
622 <h2>Settings Content</h2>
623 </TabsContent>
624
625 <TabsContent value="notifications">
626 <h2>Notifications Content</h2>
627 </TabsContent>
628 </Tabs>
629 )
630 ```
631
632 ## Example 8: Error Boundary
633
634 ```typescript
635 import { Component, ErrorInfo, ReactNode } from 'react'
636
637 interface Props {
638 children: ReactNode
639 fallback?: (error: Error, reset: () => void) => ReactNode
640 onError?: (error: Error, errorInfo: ErrorInfo) => void
641 }
642
643 interface State {
644 hasError: boolean
645 error: Error | null
646 }
647
648 class ErrorBoundary extends Component<Props, State> {
649 constructor(props: Props) {
650 super(props)
651 this.state = { hasError: false, error: null }
652 }
653
654 static getDerivedStateFromError(error: Error): State {
655 return { hasError: true, error }
656 }
657
658 componentDidCatch(error: Error, errorInfo: ErrorInfo) {
659 console.error('ErrorBoundary caught:', error, errorInfo)
660 this.props.onError?.(error, errorInfo)
661 }
662
663 reset = () => {
664 this.setState({ hasError: false, error: null })
665 }
666
667 render() {
668 if (this.state.hasError && this.state.error) {
669 if (this.props.fallback) {
670 return this.props.fallback(this.state.error, this.reset)
671 }
672
673 return (
674 <div className="error-boundary">
675 <h2>Something went wrong</h2>
676 <details>
677 <summary>Error details</summary>
678 <pre>{this.state.error.message}</pre>
679 </details>
680 <button onClick={this.reset}>Try again</button>
681 </div>
682 )
683 }
684
685 return this.props.children
686 }
687 }
688
689 // Usage
690 const App = () => (
691 <ErrorBoundary
692 fallback={(error, reset) => (
693 <div>
694 <h1>Oops! Something went wrong</h1>
695 <p>{error.message}</p>
696 <button onClick={reset}>Retry</button>
697 </div>
698 )}
699 onError={(error, errorInfo) => {
700 // Send to error tracking service
701 console.error('Error logged:', error, errorInfo)
702 }}
703 >
704 <YourApp />
705 </ErrorBoundary>
706 )
707 ```
708
709 ## Example 9: Custom Hook for Local Storage
710
711 ```typescript
712 import { useState, useEffect, useCallback } from 'react'
713
714 const useLocalStorage = <T,>(
715 key: string,
716 initialValue: T
717 ): [T, (value: T | ((val: T) => T)) => void, () => void] => {
718 // Get initial value from localStorage
719 const [storedValue, setStoredValue] = useState<T>(() => {
720 try {
721 const item = window.localStorage.getItem(key)
722 return item ? JSON.parse(item) : initialValue
723 } catch (error) {
724 console.error(`Error loading ${key} from localStorage:`, error)
725 return initialValue
726 }
727 })
728
729 // Update localStorage when value changes
730 const setValue = useCallback((value: T | ((val: T) => T)) => {
731 try {
732 const valueToStore = value instanceof Function ? value(storedValue) : value
733 setStoredValue(valueToStore)
734 window.localStorage.setItem(key, JSON.stringify(valueToStore))
735
736 // Dispatch storage event for other tabs
737 window.dispatchEvent(new Event('storage'))
738 } catch (error) {
739 console.error(`Error saving ${key} to localStorage:`, error)
740 }
741 }, [key, storedValue])
742
743 // Remove from localStorage
744 const removeValue = useCallback(() => {
745 try {
746 window.localStorage.removeItem(key)
747 setStoredValue(initialValue)
748 } catch (error) {
749 console.error(`Error removing ${key} from localStorage:`, error)
750 }
751 }, [key, initialValue])
752
753 // Listen for changes in other tabs
754 useEffect(() => {
755 const handleStorageChange = (e: StorageEvent) => {
756 if (e.key === key && e.newValue) {
757 setStoredValue(JSON.parse(e.newValue))
758 }
759 }
760
761 window.addEventListener('storage', handleStorageChange)
762 return () => window.removeEventListener('storage', handleStorageChange)
763 }, [key])
764
765 return [storedValue, setValue, removeValue]
766 }
767
768 // Usage
769 const UserPreferences = () => {
770 const [preferences, setPreferences, clearPreferences] = useLocalStorage('user-prefs', {
771 theme: 'light',
772 language: 'en',
773 notifications: true
774 })
775
776 return (
777 <div>
778 <label>
779 <input
780 type="checkbox"
781 checked={preferences.notifications}
782 onChange={e => setPreferences({
783 ...preferences,
784 notifications: e.target.checked
785 })}
786 />
787 Enable notifications
788 </label>
789
790 <button onClick={clearPreferences}>
791 Reset to defaults
792 </button>
793 </div>
794 )
795 }
796 ```
797
798 ## Example 10: Optimistic Updates with useOptimistic
799
800 ```typescript
801 'use client'
802
803 import { useOptimistic } from 'react'
804 import { likePost, unlikePost } from './actions'
805
806 interface Post {
807 id: string
808 content: string
809 likes: number
810 isLiked: boolean
811 }
812
813 const PostCard = ({ post }: { post: Post }) => {
814 const [optimisticPost, addOptimistic] = useOptimistic(
815 post,
816 (currentPost, update: Partial<Post>) => ({
817 ...currentPost,
818 ...update
819 })
820 )
821
822 const handleLike = async () => {
823 // Optimistically update UI
824 addOptimistic({
825 likes: optimisticPost.likes + 1,
826 isLiked: true
827 })
828
829 try {
830 // Send server request
831 await likePost(post.id)
832 } catch (error) {
833 // Server will send correct state via revalidation
834 console.error('Failed to like post:', error)
835 }
836 }
837
838 const handleUnlike = async () => {
839 addOptimistic({
840 likes: optimisticPost.likes - 1,
841 isLiked: false
842 })
843
844 try {
845 await unlikePost(post.id)
846 } catch (error) {
847 console.error('Failed to unlike post:', error)
848 }
849 }
850
851 return (
852 <div className="post-card">
853 <p>{optimisticPost.content}</p>
854 <button
855 onClick={optimisticPost.isLiked ? handleUnlike : handleLike}
856 className={optimisticPost.isLiked ? 'liked' : ''}
857 >
858 ❤️ {optimisticPost.likes}
859 </button>
860 </div>
861 )
862 }
863 ```
864
865 ## References
866
867 These examples demonstrate:
868 - Custom hooks for reusable logic
869 - Form handling with validation
870 - Portal usage for modals
871 - Infinite scroll with Intersection Observer
872 - Context for global state
873 - Debouncing for performance
874 - Compound components pattern
875 - Error boundaries
876 - LocalStorage integration
877 - Optimistic updates (React 19)
878
879