--- name: react-hooks-patterns user-invocable: false description: Use when React Hooks patterns including useState, useEffect, useContext, useMemo, useCallback, and custom hooks. Use for modern React development. allowed-tools: - Bash - Read --- # React Hooks Patterns Master React Hooks to build modern, functional React components. This skill covers built-in hooks, custom hooks, and advanced patterns for state management and side effects. ## useState Hook ```typescript import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = () => setCount(count + 1); const decrement = () => setCount(prev => prev - 1); return (

Count: {count}

); } // Complex state interface User { name: string; email: string; } function UserForm() { const [user, setUser] = useState({ name: '', email: '' }); const updateField = (field: keyof User, value: string) => { setUser(prev => ({ ...prev, [field]: value })); }; return (
updateField('name', e.target.value)} /> updateField('email', e.target.value)} />
); } ``` ## useEffect Hook ```typescript import { useEffect, useState } from 'react'; function DataFetcher({ userId }: { userId: number }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function fetchData() { try { setLoading(true); const response = await fetch(`/api/users/${userId}`); const result = await response.json(); if (!cancelled) { setData(result); } } catch (err) { if (!cancelled) { setError(err as Error); } } finally { if (!cancelled) { setLoading(false); } } } fetchData(); return () => { cancelled = true; }; }, [userId]); if (loading) return
Loading...
; if (error) return
Error: {error.message}
; return
{JSON.stringify(data)}
; } ``` ## useContext Hook ```typescript import { createContext, useContext, useState, ReactNode } from 'react'; interface Theme { mode: 'light' | 'dark'; toggleTheme: () => void; } const ThemeContext = createContext(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [mode, setMode] = useState<'light' | 'dark'>('light'); const toggleTheme = () => { setMode(prev => prev === 'light' ? 'dark' : 'light'); }; return ( {children} ); } export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within ThemeProvider'); } return context; } function ThemedButton() { const { mode, toggleTheme } = useTheme(); return ( ); } ``` ## useMemo and useCallback ```typescript import { useMemo, useCallback, useState } from 'react'; function ExpensiveComponent({ items }: { items: number[] }) { const [filter, setFilter] = useState(''); // Memoize expensive computation const filteredItems = useMemo(() => { console.log('Filtering items...'); return items.filter(item => item.toString().includes(filter) ); }, [items, filter]); // Memoize callback function const handleFilterChange = useCallback((value: string) => { setFilter(value); }, []); return (
handleFilterChange(e.target.value)} />
); } ``` ## Custom Hooks ```typescript // useLocalStorage hook function useLocalStorage(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }; return [storedValue, setValue] as const; } // useDebounce hook function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } // Usage function SearchComponent() { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 500); useEffect(() => { if (debouncedSearchTerm) { // Perform search console.log('Searching for:', debouncedSearchTerm); } }, [debouncedSearchTerm]); return ( setSearchTerm(e.target.value)} /> ); } ``` ## useReducer for Complex State ```typescript import { useReducer } from 'react'; interface State { count: number; history: number[]; } type Action = | { type: 'INCREMENT' } | { type: 'DECREMENT' } | { type: 'RESET' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'INCREMENT': return { count: state.count + 1, history: [...state.history, state.count + 1] }; case 'DECREMENT': return { count: state.count - 1, history: [...state.history, state.count - 1] }; case 'RESET': return { count: 0, history: [0] }; default: return state; } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0, history: [0] }); return (

Count: {state.count}

History: {state.history.join(', ')}

); } // Complex form state with useReducer interface FormState { values: { name: string; email: string; age: number; }; errors: { name?: string; email?: string; age?: string; }; touched: { name: boolean; email: boolean; age: boolean; }; isSubmitting: boolean; } type FormAction = | { type: 'SET_FIELD'; field: string; value: string | number } | { type: 'SET_ERROR'; field: string; error: string } | { type: 'SET_TOUCHED'; field: string } | { type: 'SUBMIT_START' } | { type: 'SUBMIT_SUCCESS' } | { type: 'SUBMIT_ERROR' } | { type: 'RESET' }; function formReducer(state: FormState, action: FormAction): FormState { switch (action.type) { case 'SET_FIELD': return { ...state, values: { ...state.values, [action.field]: action.value } }; case 'SET_ERROR': return { ...state, errors: { ...state.errors, [action.field]: action.error } }; case 'SET_TOUCHED': return { ...state, touched: { ...state.touched, [action.field]: true } }; case 'SUBMIT_START': return { ...state, isSubmitting: true }; case 'SUBMIT_SUCCESS': return { ...state, isSubmitting: false }; case 'SUBMIT_ERROR': return { ...state, isSubmitting: false }; case 'RESET': return { values: { name: '', email: '', age: 0 }, errors: {}, touched: { name: false, email: false, age: false }, isSubmitting: false }; default: return state; } } function ComplexForm() { const [state, dispatch] = useReducer(formReducer, { values: { name: '', email: '', age: 0 }, errors: {}, touched: { name: false, email: false, age: false }, isSubmitting: false }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); dispatch({ type: 'SUBMIT_START' }); try { await submitForm(state.values); dispatch({ type: 'SUBMIT_SUCCESS' }); } catch (error) { dispatch({ type: 'SUBMIT_ERROR' }); } }; return (
dispatch({ type: 'SET_FIELD', field: 'name', value: e.target.value })} onBlur={() => dispatch({ type: 'SET_TOUCHED', field: 'name' })} /> {state.touched.name && state.errors.name && ( {state.errors.name} )}
); } ``` ## useRef Hook ```typescript import { useRef, useEffect, useState } from 'react'; function FocusInput() { const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); return ; } // Storing mutable values function Timer() { const intervalRef = useRef(null); const [count, setCount] = useState(0); const start = () => { if (intervalRef.current !== null) return; intervalRef.current = window.setInterval(() => { setCount(c => c + 1); }, 1000); }; const stop = () => { if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; } }; useEffect(() => { return () => { if (intervalRef.current !== null) { clearInterval(intervalRef.current); } }; }, []); return (

Count: {count}

); } // Previous value tracking function usePrevious(value: T): T | undefined { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } function CounterWithPrevious() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return (

Current: {count}

Previous: {prevCount}

); } ``` ## useLayoutEffect for DOM Measurements ```typescript import { useLayoutEffect, useRef, useState } from 'react'; // Measure element dimensions before paint function TooltipWithMeasurement() { const [tooltipHeight, setTooltipHeight] = useState(0); const tooltipRef = useRef(null); useLayoutEffect(() => { if (tooltipRef.current) { const { height } = tooltipRef.current.getBoundingClientRect(); setTooltipHeight(height); } }, []); return (
Tooltip content
); } // Synchronize scroll positions function SyncedScrollPanels() { const leftRef = useRef(null); const rightRef = useRef(null); useLayoutEffect(() => { const left = leftRef.current; const right = rightRef.current; if (!left || !right) return; const syncScroll = (source: HTMLDivElement, target: HTMLDivElement) => { return () => { target.scrollTop = source.scrollTop; }; }; const leftHandler = syncScroll(left, right); const rightHandler = syncScroll(right, left); left.addEventListener('scroll', leftHandler); right.addEventListener('scroll', rightHandler); return () => { left.removeEventListener('scroll', leftHandler); right.removeEventListener('scroll', rightHandler); }; }, []); return (
Left panel content
Right panel content
); } ``` ## useImperativeHandle with forwardRef ```typescript import { useRef, useImperativeHandle, forwardRef, useState } from 'react'; // Define exposed methods interface interface VideoPlayerHandle { play: () => void; pause: () => void; seek: (time: number) => void; } interface VideoPlayerProps { src: string; } const VideoPlayer = forwardRef( (props, ref) => { const videoRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); useImperativeHandle(ref, () => ({ play: () => { videoRef.current?.play(); setIsPlaying(true); }, pause: () => { videoRef.current?.pause(); setIsPlaying(false); }, seek: (time: number) => { if (videoRef.current) { videoRef.current.currentTime = time; } } }), []); return (
); } ); function ParentComponent() { const playerRef = useRef(null); return (
); } // Input with custom imperative methods interface InputHandle { focus: () => void; clear: () => void; getValue: () => string; } const CustomInput = forwardRef( (props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus(); }, clear: () => { if (inputRef.current) { inputRef.current.value = ''; } }, getValue: () => { return inputRef.current?.value || ''; } }), []); return ; } ); ``` ## Custom Hooks Composition Patterns ```typescript import { useState, useEffect, useCallback } from 'react'; // Composing multiple hooks together function useAsync(asyncFunction: () => Promise) { const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle'); const [value, setValue] = useState(null); const [error, setError] = useState(null); const execute = useCallback(() => { setStatus('pending'); setValue(null); setError(null); return asyncFunction() .then((response) => { setValue(response); setStatus('success'); }) .catch((error) => { setError(error); setStatus('error'); }); }, [asyncFunction]); return { execute, status, value, error }; } // Composing useAsync with other hooks function useFetch(url: string) { const fetchData = useCallback( () => fetch(url).then((res) => res.json() as Promise), [url] ); const { execute, status, value, error } = useAsync(fetchData); useEffect(() => { execute(); }, [execute]); return { data: value, loading: status === 'pending', error }; } // Hook that composes multiple custom hooks function useForm>(initialValues: T) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState>>({}); const [touched, setTouched] = useState>>({}); const [isSubmitting, setIsSubmitting] = useState(false); const handleChange = useCallback((field: keyof T, value: any) => { setValues((prev) => ({ ...prev, [field]: value })); }, []); const handleBlur = useCallback((field: keyof T) => { setTouched((prev) => ({ ...prev, [field]: true })); }, []); const handleSubmit = useCallback( async ( onSubmit: (values: T) => Promise, validate?: (values: T) => Partial> ) => { if (validate) { const validationErrors = validate(values); setErrors(validationErrors); if (Object.keys(validationErrors).length > 0) return; } setIsSubmitting(true); try { await onSubmit(values); } finally { setIsSubmitting(false); } }, [values] ); const reset = useCallback(() => { setValues(initialValues); setErrors({}); setTouched({}); setIsSubmitting(false); }, [initialValues]); return { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit, reset }; } // Using composed hooks function UserProfileForm() { const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit, reset } = useForm({ name: '', email: '', bio: '' }); const validate = (vals: typeof values) => { const errs: Partial> = {}; if (!vals.name) errs.name = 'Name is required'; if (!vals.email) errs.email = 'Email is required'; return errs; }; return (
{ e.preventDefault(); handleSubmit( async (vals) => { await saveProfile(vals); }, validate ); }} > handleChange('name', e.target.value)} onBlur={() => handleBlur('name')} /> {touched.name && errors.name && {errors.name}}
); } ``` ## Advanced useCallback and useMemo Optimization ```typescript import { useState, useCallback, useMemo, memo } from 'react'; // Complex memoization scenario interface Item { id: number; name: string; category: string; price: number; } interface Props { items: Item[]; } const ItemList = memo(({ items }: Props) => { return (
    {items.map((item) => (
  • {item.name}
  • ))}
); }); function OptimizedShop() { const [items] = useState([ { id: 1, name: 'Apple', category: 'fruit', price: 1.5 }, { id: 2, name: 'Banana', category: 'fruit', price: 0.8 }, { id: 3, name: 'Carrot', category: 'vegetable', price: 1.2 } ]); const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); const [sortBy, setSortBy] = useState<'name' | 'price'>('name'); // Memoize filtered items const filteredItems = useMemo(() => { return items.filter((item) => { const matchesSearch = item.name .toLowerCase() .includes(searchTerm.toLowerCase()); const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory; return matchesSearch && matchesCategory; }); }, [items, searchTerm, selectedCategory]); // Memoize sorted items const sortedItems = useMemo(() => { return [...filteredItems].sort((a, b) => { if (sortBy === 'name') { return a.name.localeCompare(b.name); } return a.price - b.price; }); }, [filteredItems, sortBy]); // Memoize categories list const categories = useMemo(() => { const uniqueCategories = new Set(items.map((item) => item.category)); return ['all', ...Array.from(uniqueCategories)]; }, [items]); // Memoize callbacks const handleSearch = useCallback((value: string) => { setSearchTerm(value); }, []); const handleCategoryChange = useCallback((category: string) => { setSelectedCategory(category); }, []); const handleSortChange = useCallback((sort: 'name' | 'price') => { setSortBy(sort); }, []); return (
handleSearch(e.target.value)} placeholder="Search items..." />
); } // Factory pattern with useCallback function useEventCallback any>(fn: T): T { const ref = useRef(fn); useLayoutEffect(() => { ref.current = fn; }); return useCallback( ((...args) => ref.current(...args)) as T, [] ); } // Usage of useEventCallback function FormWithEventCallback() { const [count, setCount] = useState(0); // This callback always has access to latest count // but maintains stable reference const handleSubmit = useEventCallback(() => { console.log('Current count:', count); }); return (

Count: {count}

); } ``` ## Advanced Hook Patterns ```typescript import { useState, useEffect, useCallback, useRef } from 'react'; // useInterval - Declarative interval hook function useInterval(callback: () => void, delay: number | null) { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { if (delay === null) return; const id = setInterval(() => savedCallback.current(), delay); return () => clearInterval(id); }, [delay]); } function Clock() { const [time, setTime] = useState(new Date()); useInterval(() => { setTime(new Date()); }, 1000); return
{time.toLocaleTimeString()}
; } // useOnScreen - Detect if element is visible function useOnScreen(ref: React.RefObject) { const [isIntersecting, setIntersecting] = useState(false); useEffect(() => { if (!ref.current) return; const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting) ); observer.observe(ref.current); return () => { observer.disconnect(); }; }, [ref]); return isIntersecting; } function LazyImage({ src, alt }: { src: string; alt: string }) { const ref = useRef(null); const isVisible = useOnScreen(ref); return (
{isVisible ? ( {alt} ) : (
Loading...
)}
); } // useMediaQuery - Responsive design hook function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); if (media.matches !== matches) { setMatches(media.matches); } const listener = () => setMatches(media.matches); media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, [matches, query]); return matches; } function ResponsiveComponent() { const isMobile = useMediaQuery('(max-width: 768px)'); const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); const isDesktop = useMediaQuery('(min-width: 1025px)'); return (
{isMobile &&
Mobile View
} {isTablet &&
Tablet View
} {isDesktop &&
Desktop View
}
); } // useClickOutside - Detect clicks outside element function useClickOutside( ref: React.RefObject, handler: (event: MouseEvent | TouchEvent) => void ) { useEffect(() => { const listener = (event: MouseEvent | TouchEvent) => { if (!ref.current || ref.current.contains(event.target as Node)) { return; } handler(event); }; document.addEventListener('mousedown', listener); document.addEventListener('touchstart', listener); return () => { document.removeEventListener('mousedown', listener); document.removeEventListener('touchstart', listener); }; }, [ref, handler]); } function Dropdown() { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); useClickOutside(ref, () => setIsOpen(false)); return (
{isOpen &&
Dropdown Content
}
); } // useToggle - Boolean state management function useToggle(initialValue = false): [boolean, () => void] { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => setValue((v) => !v), []); return [value, toggle]; } function ToggleExample() { const [isOn, toggle] = useToggle(false); return (

The switch is {isOn ? 'ON' : 'OFF'}

); } // useArray - Array manipulation hook function useArray(initialValue: T[]) { const [array, setArray] = useState(initialValue); const push = useCallback((element: T) => { setArray((a) => [...a, element]); }, []); const filter = useCallback((callback: (item: T) => boolean) => { setArray((a) => a.filter(callback)); }, []); const update = useCallback((index: number, newElement: T) => { setArray((a) => [ ...a.slice(0, index), newElement, ...a.slice(index + 1) ]); }, []); const remove = useCallback((index: number) => { setArray((a) => [...a.slice(0, index), ...a.slice(index + 1)]); }, []); const clear = useCallback(() => { setArray([]); }, []); return { array, set: setArray, push, filter, update, remove, clear }; } function TodoList() { const { array: todos, push, remove, update } = useArray<{ id: number; text: string; completed: boolean; }>([]); const addTodo = (text: string) => { push({ id: Date.now(), text, completed: false }); }; const toggleTodo = (index: number) => { const todo = todos[index]; update(index, { ...todo, completed: !todo.completed }); }; return (
{todos.map((todo, index) => (
toggleTodo(index)} /> {todo.text}
))}
); } ``` ## When to Use This Skill Use react-hooks-patterns when you need to: - Build modern React applications with functional components - Manage component state with useState and useReducer - Handle side effects with useEffect - Share state across components with useContext - Optimize performance with useMemo and useCallback - Create reusable logic with custom hooks - Access DOM elements with useRef - Build maintainable React applications - Follow React best practices and patterns ## Best Practices - Use functional updates when new state depends on previous state - Always clean up side effects in useEffect return function - Include all dependencies in useEffect dependency array - Use useCallback to memoize functions passed to child components - Use useMemo only for expensive computations, not simple values - Create custom hooks to encapsulate and reuse stateful logic - Use useReducer for complex state logic with multiple sub-values - Keep hooks at the top level of components, never in conditions - Name custom hooks with "use" prefix for linting and conventions - Use TypeScript for type safety and better developer experience - Separate concerns by creating focused custom hooks - Use useRef for values that don't trigger re-renders - Prefer useLayoutEffect only when measuring DOM or preventing flicker - Use memo() with components that receive callback props - Compose hooks to build more complex behaviors from simple ones - Use useImperativeHandle sparingly, prefer declarative patterns - Avoid premature optimization with useMemo and useCallback - Keep dependency arrays honest, use ESLint exhaustive-deps rule - Extract complex logic into custom hooks for testability - Use useContext for global state, not prop drilling ## Common Pitfalls - Forgetting to include dependencies in useEffect array - Not cleaning up side effects leading to memory leaks - Overusing useCallback and useMemo causing premature optimization - Calling hooks conditionally or inside loops (violates Rules of Hooks) - Not handling async operations properly in useEffect - Creating infinite loops by updating state in useEffect incorrectly - Mutating ref.current during render instead of in effects - Using stale closures in callbacks without proper dependencies - Not using functional updates with useState when needed - Setting state on unmounted components - Using object or array literals in dependency arrays - Not memoizing expensive calculations that run on every render - Confusing useEffect with useLayoutEffect use cases - Creating unnecessary re-renders by not memoizing callbacks - Using useState for values that should be refs - Not using cleanup functions for event listeners and subscriptions - Forgetting that useEffect runs after paint, not before - Creating tightly coupled custom hooks that are hard to reuse - Over-abstracting with custom hooks too early - Ignoring ESLint warnings about dependency arrays ## Resources ### Official React Documentation - [React Hooks API Reference](https://react.dev/reference/react) - [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) - [useState Hook](https://react.dev/reference/react/useState) - [useEffect Hook](https://react.dev/reference/react/useEffect) - [useContext Hook](https://react.dev/reference/react/useContext) - [useReducer Hook](https://react.dev/reference/react/useReducer) - [useCallback Hook](https://react.dev/reference/react/useCallback) - [useMemo Hook](https://react.dev/reference/react/useMemo) - [useRef Hook](https://react.dev/reference/react/useRef) - [useLayoutEffect Hook](https://react.dev/reference/react/useLayoutEffect) - [useImperativeHandle Hook](https://react.dev/reference/react/useImperativeHandle) ### Guides and Best Practices - [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) - [Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects) - [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) - [Separating Events from Effects](https://react.dev/learn/separating-events-from-effects) - [Removing Effect Dependencies](https://react.dev/learn/removing-effect-dependencies) - [Lifecycle of Reactive Effects](https://react.dev/learn/lifecycle-of-reactive-effects) ### TypeScript Resources - [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/) - [React TypeScript Hooks](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks) - [Advanced React TypeScript](https://react-typescript-cheatsheet.netlify.app/docs/advanced/intro) ### Additional Resources - [usehooks.com - Custom Hooks Collection](https://usehooks.com/) - [usehooks-ts - TypeScript Hooks](https://usehooks-ts.com/) - [React Hooks FAQ](https://react.dev/learn#using-hooks) - [ESLint Plugin React Hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)