# Hook TypeScript Patterns Type-safe hook patterns for useState, useRef, useReducer, useContext, and custom hooks. ## useState Type inference works for simple types; explicit typing needed for unions/null. ```typescript // Inference works const [count, setCount] = useState(0); // number const [name, setName] = useState(''); // string const [items, setItems] = useState([]); // explicit for empty arrays // Explicit for unions/null const [user, setUser] = useState(null); const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle'); // Complex initial state type FormData = { name: string; email: string }; const [formData, setFormData] = useState({ name: '', email: '', }); // Lazy initialization const [data, setData] = useState(() => { const cached = localStorage.getItem('data'); return cached ? JSON.parse(cached) : defaultData; }); ``` ## useRef Distinguish DOM refs (null initial) from mutable value refs (value initial). ```typescript // DOM element ref - null initial, readonly .current const inputRef = useRef(null); const buttonRef = useRef(null); const divRef = useRef(null); useEffect(() => { inputRef.current?.focus(); // Optional chaining for null }, []); // Mutable value ref - non-null initial, mutable .current const countRef = useRef(0); countRef.current += 1; // No optional chaining const previousValueRef = useRef(undefined); previousValueRef.current = currentValue; // Interval/timeout ref const timeoutRef = useRef>(); const intervalRef = useRef>(); useEffect(() => { timeoutRef.current = setTimeout(() => {}, 1000); return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); // Callback ref for dynamic elements const callbackRef = useCallback((node: HTMLDivElement | null) => { if (node) { node.scrollIntoView({ behavior: 'smooth' }); } }, []); ``` ## useReducer Typed actions with discriminated unions. ```typescript type State = { count: number; status: 'idle' | 'loading' | 'success' | 'error'; error?: string; }; type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'set'; payload: number } | { type: 'setStatus'; payload: State['status'] } | { type: 'setError'; payload: string }; function reducer(state: State, action: Action): State { switch (action.type) { case 'increment': return { ...state, count: state.count + 1 }; case 'decrement': return { ...state, count: state.count - 1 }; case 'set': return { ...state, count: action.payload }; case 'setStatus': return { ...state, status: action.payload }; case 'setError': return { ...state, status: 'error', error: action.payload }; default: return state; } } function Component() { const [state, dispatch] = useReducer(reducer, { count: 0, status: 'idle', }); dispatch({ type: 'set', payload: 10 }); // Type-safe dispatch({ type: 'set' }); // Error: payload required dispatch({ type: 'unknown' }); // Error: invalid action type } ``` ### Reducer with Context ```typescript type AuthState = { user: User | null; isAuthenticated: boolean; }; type AuthAction = | { type: 'login'; payload: User } | { type: 'logout' }; type AuthContextValue = { state: AuthState; dispatch: React.Dispatch; }; const AuthContext = createContext(null); function authReducer(state: AuthState, action: AuthAction): AuthState { switch (action.type) { case 'login': return { user: action.payload, isAuthenticated: true }; case 'logout': return { user: null, isAuthenticated: false }; } } function AuthProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(authReducer, { user: null, isAuthenticated: false, }); return ( {children} ); } function useAuth() { const context = useContext(AuthContext); if (!context) throw new Error('useAuth must be used within AuthProvider'); return context; } ``` ## useContext Typed context with and without default values. ```typescript // Context with default value type Theme = 'light' | 'dark'; const ThemeContext = createContext('light'); function useTheme() { return useContext(ThemeContext); // Always Theme, never null } // Context without default (must handle null) type User = { id: string; name: string }; const UserContext = createContext(null); function useUser() { const user = useContext(UserContext); if (!user) throw new Error('useUser must be used within UserProvider'); return user; // Type narrowed to User } // Context with complex value type AppContextValue = { theme: Theme; user: User | null; setTheme: (theme: Theme) => void; login: (user: User) => void; logout: () => void; }; const AppContext = createContext(null); function useApp() { const context = useContext(AppContext); if (!context) throw new Error('useApp must be used within AppProvider'); return context; } ``` ## Custom Hooks Return type patterns for simple and complex hooks. ```typescript // Object return - properties accessed by name function useCounter(initial: number) { const [count, setCount] = useState(initial); const increment = () => setCount((c) => c + 1); const decrement = () => setCount((c) => c - 1); const reset = () => setCount(initial); return { count, increment, decrement, reset }; } // Usage const { count, increment } = useCounter(0); // Tuple return - positional destructuring function useToggle(initial = false): [boolean, () => void, () => void, () => void] { const [value, setValue] = useState(initial); const toggle = () => setValue((v) => !v); const setTrue = () => setValue(true); const setFalse = () => setValue(false); return [value, toggle, setTrue, setFalse]; } // Usage const [isOpen, toggleOpen, open, close] = useToggle(); // as const for tuple inference function useLocalStorage(key: string, initial: T) { const [value, setValue] = useState(() => { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : initial; }); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue] as const; // readonly tuple } // Generic custom hook function useFetch(url: string) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; fetch(url) .then((res) => res.json()) .then((json: T) => { if (!cancelled) { setData(json); setLoading(false); } }) .catch((err) => { if (!cancelled) { setError(err); setLoading(false); } }); return () => { cancelled = true; }; }, [url]); return { data, loading, error }; } // Usage - T inferred from usage or explicit const { data } = useFetch('/api/users'); ``` ## useCallback and useMemo Typed callbacks and memoized values. ```typescript // useCallback with typed parameters const handleClick = useCallback((id: string, event: React.MouseEvent) => { console.log(id, event.target); }, []); // useCallback returning value const formatDate = useCallback((date: Date): string => { return date.toLocaleDateString(); }, []); // useMemo with explicit return type const sortedItems = useMemo((): Item[] => { return [...items].sort((a, b) => a.name.localeCompare(b.name)); }, [items]); // useMemo with complex computation const stats = useMemo(() => { return { total: items.length, average: items.reduce((a, b) => a + b.value, 0) / items.length, max: Math.max(...items.map((i) => i.value)), }; }, [items]); ``` ## useImperativeHandle Expose imperative methods from components. ```typescript type InputHandle = { focus: () => void; clear: () => void; getValue: () => string; }; type InputProps = { ref?: React.Ref; label: string; }; function CustomInput({ ref, label }: InputProps) { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), clear: () => { if (inputRef.current) inputRef.current.value = ''; }, getValue: () => inputRef.current?.value ?? '', })); return (
); } // Usage function Form() { const inputRef = useRef(null); const handleSubmit = () => { const value = inputRef.current?.getValue(); inputRef.current?.clear(); }; return (
); } ``` ## useLayoutEffect Same signature as useEffect, runs synchronously after DOM mutations. ```typescript function Tooltip({ targetRef }: { targetRef: React.RefObject }) { const tooltipRef = useRef(null); const [position, setPosition] = useState({ top: 0, left: 0 }); useLayoutEffect(() => { if (targetRef.current && tooltipRef.current) { const rect = targetRef.current.getBoundingClientRect(); setPosition({ top: rect.bottom + 8, left: rect.left, }); } }, [targetRef]); return (
Tooltip content
); } ``` ## useId Generate unique IDs for accessibility. ```typescript function FormField({ label }: { label: string }) { const id = useId(); const errorId = useId(); return (
Error message
); } ``` ## useSyncExternalStore Subscribe to external stores with SSR support. ```typescript type Store = { getState: () => T; subscribe: (callback: () => void) => () => void; }; function useStore(store: Store): T { return useSyncExternalStore( store.subscribe, store.getState, store.getState // Server snapshot ); } // Example: window width store const widthStore: Store = { getState: () => (typeof window !== 'undefined' ? window.innerWidth : 0), subscribe: (callback) => { window.addEventListener('resize', callback); return () => window.removeEventListener('resize', callback); }, }; function useWindowWidth() { return useSyncExternalStore( widthStore.subscribe, widthStore.getState, () => 0 // Server fallback ); } ```