/** * TypeScript React Patterns * * This file demonstrates type-safe React patterns including: * - Component props typing * - Hooks with TypeScript * - Context with type safety * - Generic components * - Event handlers * - Ref types */ import { createContext, useContext, useEffect, useReducer, useRef, useState } from 'react' import type { ReactNode, InputHTMLAttributes, FormEvent, ChangeEvent } from 'react' // ============================================================================ // Component Props Patterns // ============================================================================ // Basic component with props interface ButtonProps { variant?: 'primary' | 'secondary' | 'tertiary' size?: 'sm' | 'md' | 'lg' disabled?: boolean onClick?: () => void children: ReactNode } export function Button({ variant = 'primary', size = 'md', disabled = false, onClick, children, }: ButtonProps) { return ( ) } // Props extending HTML attributes interface InputProps extends InputHTMLAttributes { label?: string error?: string helperText?: string } export function Input({ label, error, helperText, ...inputProps }: InputProps) { return (
{label && } {error && {error}} {helperText && {helperText}}
) } // Generic component interface ListProps { items: T[] renderItem: (item: T, index: number) => ReactNode keyExtractor: (item: T, index: number) => string emptyMessage?: string } export function List({ items, renderItem, keyExtractor, emptyMessage = 'No items', }: ListProps) { if (items.length === 0) { return
{emptyMessage}
} return (
    {items.map((item, index) => (
  • {renderItem(item, index)}
  • ))}
) } // Component with children render prop interface ContainerProps { isLoading: boolean error: Error | null children: (props: { retry: () => void }) => ReactNode } export function Container({ isLoading, error, children }: ContainerProps) { const retry = () => { // Retry logic } if (isLoading) return
Loading...
if (error) return
Error: {error.message}
return <>{children({ retry })} } // ============================================================================ // Hooks Patterns // ============================================================================ // useState with explicit type function useCounter(initialValue: number = 0) { const [count, setCount] = useState(initialValue) const increment = () => setCount((c) => c + 1) const decrement = () => setCount((c) => c - 1) const reset = () => setCount(initialValue) return { count, increment, decrement, reset } } // useState with union type type LoadingState = 'idle' | 'loading' | 'success' | 'error' function useLoadingState() { const [state, setState] = useState('idle') const startLoading = () => setState('loading') const setSuccess = () => setState('success') const setError = () => setState('error') const reset = () => setState('idle') return { state, startLoading, setSuccess, setError, reset } } // Custom hook with options interface UseFetchOptions { initialData?: T onSuccess?: (data: T) => void onError?: (error: Error) => void } interface UseFetchReturn { data: T | undefined loading: boolean error: Error | null refetch: () => Promise } function useFetch(url: string, options?: UseFetchOptions): UseFetchReturn { const [data, setData] = useState(options?.initialData) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const fetchData = async () => { setLoading(true) setError(null) try { const response = await fetch(url) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const json = await response.json() setData(json) options?.onSuccess?.(json) } catch (err) { const error = err instanceof Error ? err : new Error(String(err)) setError(error) options?.onError?.(error) } finally { setLoading(false) } } useEffect(() => { fetchData() }, [url]) return { data, loading, error, refetch: fetchData } } // useReducer with discriminated unions interface User { id: string name: string email: string } type FetchState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error } type FetchAction = | { type: 'FETCH_START' } | { type: 'FETCH_SUCCESS'; payload: T } | { type: 'FETCH_ERROR'; error: Error } | { type: 'RESET' } function fetchReducer(state: FetchState, action: FetchAction): FetchState { switch (action.type) { case 'FETCH_START': return { status: 'loading' } case 'FETCH_SUCCESS': return { status: 'success', data: action.payload } case 'FETCH_ERROR': return { status: 'error', error: action.error } case 'RESET': return { status: 'idle' } } } function useFetchWithReducer(url: string) { const [state, dispatch] = useReducer(fetchReducer, { status: 'idle' }) useEffect(() => { let isCancelled = false const fetchData = async () => { dispatch({ type: 'FETCH_START' }) try { const response = await fetch(url) const data = await response.json() if (!isCancelled) { dispatch({ type: 'FETCH_SUCCESS', payload: data }) } } catch (error) { if (!isCancelled) { dispatch({ type: 'FETCH_ERROR', error: error instanceof Error ? error : new Error(String(error)), }) } } } fetchData() return () => { isCancelled = true } }, [url]) return state } // ============================================================================ // Context Patterns // ============================================================================ // Type-safe context interface AuthContextType { user: User | null isAuthenticated: boolean login: (email: string, password: string) => Promise logout: () => void } const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const login = async (email: string, password: string) => { // Login logic const userData = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }), }).then((r) => r.json()) setUser(userData) } const logout = () => { setUser(null) } const value: AuthContextType = { user, isAuthenticated: user !== null, login, logout, } return {children} } // Custom hook with error handling export function useAuth(): AuthContextType { const context = useContext(AuthContext) if (context === undefined) { throw new Error('useAuth must be used within AuthProvider') } return context } // ============================================================================ // Event Handler Patterns // ============================================================================ interface FormData { name: string email: string message: string } function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '', message: '', }) // Type-safe change handler const handleChange = (e: ChangeEvent) => { const { name, value } = e.target setFormData((prev) => ({ ...prev, [name]: value, })) } // Type-safe submit handler const handleSubmit = (e: FormEvent) => { e.preventDefault() console.log('Submitting:', formData) } // Specific field handler const handleNameChange = (e: ChangeEvent) => { setFormData((prev) => ({ ...prev, name: e.target.value })) } return (