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.

Component Patterns

Composition Over Inheritance

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

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

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

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

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

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

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}

Performance Optimization

Memoization

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

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

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}

Form Handling Patterns

Controlled Form with Validation

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

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

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

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

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.