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
import { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
const increment = () => setCount(count + 1); const decrement = () => setCount(prev => prev - 1);
return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick=
{decrement}>-</button> </div> ); }
// Complex state interface User { name: string; email: string; }
function UserForm() { const [user, setUser] = useState<User>({ name: '', email: '' });
const updateField = (field: keyof User, value: string) => { setUser(prev => ({ ...prev, [field]: value })); };
return ( <form> <input value={user.name} onChange={(e) => updateField('name', e.target.value)} /> <input value={user.email} onChange={(e) => updateField('email', e.target.value)} /> </form> ); }
useEffect Hook
import { useEffect, useState } from 'react';
function DataFetcher({ userId }: { userId: number }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(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 <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>{JSON.stringify(data)}</div>; }
useContext Hook
import { createContext, useContext, useState, ReactNode } from 'react';
interface Theme { mode: 'light' | 'dark'; toggleTheme: () => void; }
const ThemeContext = createContext<Theme | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) { const [mode, setMode] = useState<'light' | 'dark'>('light');
const toggleTheme = () => { setMode(prev => prev === 'light' ? 'dark' : 'light'); };
return ( <ThemeContext.Provider value={{ mode, toggleTheme }}> {children} </ThemeContext.Provider> ); }
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 ( <button onClick={toggleTheme}> Current mode: {mode} </button> ); }
useMemo and useCallback
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 ( <div> <input value={filter} onChange={(e) => handleFilterChange(e.target.value)} /> <ItemList items={filteredItems} /> </div> ); }
Custom Hooks
// useLocalStorage hook function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { 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<T>(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 ( <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> ); }
useReducer for Complex State
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 ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button> <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button> <p>History: {state.history.join(', ')}</p> </div> ); }
// 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 ( <form onSubmit={handleSubmit}> <input value={state.values.name} onChange={(e) => dispatch({ type: 'SET_FIELD', field: 'name', value: e.target.value })} onBlur={() => dispatch({ type: 'SET_TOUCHED', field: 'name' })} /> {state.touched.name && state.errors.name && ( <span>{state.errors.name}</span> )} <button type="submit" disabled={state.isSubmitting}> Submit </button> </form> ); }
useRef Hook
import { useRef, useEffect, useState } from 'react';
function FocusInput() { const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />; }
// Storing mutable values function Timer() { const intervalRef = useRef<number | null>(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 ( <div> <p>Count: {count}</p> <button onClick={start}>Start</button> <button onClick={stop}>Stop</button> </div> ); }
// Previous value tracking function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; }
function CounterWithPrevious() { const [count, setCount] = useState(0); const prevCount = usePrevious(count);
return ( <div> <p>Current: {count}</p> <p>Previous: {prevCount}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
useLayoutEffect for DOM Measurements
import { useLayoutEffect, useRef, useState } from 'react';
// Measure element dimensions before paint function TooltipWithMeasurement() { const [tooltipHeight, setTooltipHeight] = useState(0); const tooltipRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => { if (tooltipRef.current) { const { height } = tooltipRef.current.getBoundingClientRect(); setTooltipHeight(height); } }, []);
return (
<div>
<div
ref={tooltipRef}
style={{
position: 'absolute',
top: calc(100% + ${tooltipHeight}px)
}}
>
Tooltip content
</div>
</div>
);
}
// Synchronize scroll positions function SyncedScrollPanels() { const leftRef = useRef<HTMLDivElement>(null); const rightRef = useRef<HTMLDivElement>(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 ( <div style={{ display: 'flex' }}> <div ref={leftRef} style={{ overflow: 'auto', height: 300 }}> Left panel content </div> <div ref={rightRef} style={{ overflow: 'auto', height: 300 }}> Right panel content </div> </div> ); }
useImperativeHandle with forwardRef
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<VideoPlayerHandle, VideoPlayerProps>( (props, ref) => { const videoRef = useRef<HTMLVideoElement>(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 (
<div>
<video ref={videoRef} src={props.src} />
<p>Status: {isPlaying ? 'Playing' : 'Paused'}</p>
</div>
);
} );
function ParentComponent() { const playerRef = useRef<VideoPlayerHandle>(null);
return ( <div> <VideoPlayer ref={playerRef} src="video.mp4" /> <button onClick={() => playerRef.current?.play()}> Play </button> <button onClick={() => playerRef.current?.pause()}> Pause </button> <button onClick={() => playerRef.current?.seek(30)}> Skip to 30s </button> </div> ); }
// Input with custom imperative methods interface InputHandle { focus: () => void; clear: () => void; getValue: () => string; }
const CustomInput = forwardRef<InputHandle, { placeholder?: string }>( (props, ref) => { const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
getValue: () => {
return inputRef.current?.value || '';
}
}), []);
return <input ref={inputRef} placeholder={props.placeholder} />;
} );
Custom Hooks Composition Patterns
import { useState, useEffect, useCallback } from 'react';
// Composing multiple hooks together function useAsync<T>(asyncFunction: () => Promise<T>) { const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle'); const [value, setValue] = useState<T | null>(null); const [error, setError] = useState<Error | null>(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<T>(url: string) { const fetchData = useCallback( () => fetch(url).then((res) => res.json() as Promise<T>), [url] );
const { execute, status, value, error } = useAsync<T>(fetchData);
useEffect(() => { execute(); }, [execute]);
return { data: value, loading: status === 'pending', error }; }
// Hook that composes multiple custom hooks function useForm<T extends Record<string, any>>(initialValues: T) { const [values, setValues] = useState<T>(initialValues); const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({}); 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<void>, validate?: (values: T) => Partial<Record<keyof T, string>> ) => { 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<Record<keyof typeof values, string>> = {}; if (!vals.name) errs.name = 'Name is required'; if (!vals.email) errs.email = 'Email is required'; return errs; };
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit( async (vals) => { await saveProfile(vals); }, validate ); }} > <input value={values.name} onChange={(e) => handleChange('name', e.target.value)} onBlur={() => handleBlur('name')} /> {touched.name && errors.name && <span>{errors.name}</span>} <button type="submit" disabled={isSubmitting}> Save </button> <button type="button" onClick={reset}> Reset </button> </form> ); }
Advanced useCallback and useMemo Optimization
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 ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); });
function OptimizedShop() { const [items] = useState<Item[]>([ { 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<string>('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 ( <div> <input value={searchTerm} onChange={(e) => handleSearch(e.target.value)} placeholder="Search items..." /> <select value={selectedCategory} onChange={(e) => handleCategoryChange(e.target.value)} > {categories.map((cat) => ( <option key={cat} value={cat}> {cat} </option> ))} </select> <select value={sortBy} onChange={(e) => handleSortChange(e.target.value as 'name' | 'price')} > <option value="name">Sort by Name</option> <option value="price">Sort by Price</option> </select> <ItemList items={sortedItems} /> </div> ); }
// Factory pattern with useCallback function useEventCallback<T extends (...args: any[]) => any>(fn: T): T { const ref = useRef<T>(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 ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <ExpensiveChild onSubmit={handleSubmit} /> </div> ); }
Advanced Hook Patterns
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 <div>{time.toLocaleTimeString()}</div>; }
// useOnScreen - Detect if element is visible function useOnScreen(ref: React.RefObject<HTMLElement>) { 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<HTMLDivElement>(null); const isVisible = useOnScreen(ref);
return ( <div ref={ref}> {isVisible ? ( <img src={src} alt={alt} /> ) : ( <div>Loading...</div> )} </div> ); }
// 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 ( <div> {isMobile && <div>Mobile View</div>} {isTablet && <div>Tablet View</div>} {isDesktop && <div>Desktop View</div>} </div> ); }
// useClickOutside - Detect clicks outside element function useClickOutside( ref: React.RefObject<HTMLElement>, 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<HTMLDivElement>(null);
useClickOutside(ref, () => setIsOpen(false));
return ( <div ref={ref}> <button onClick={() => setIsOpen(!isOpen)}>Toggle</button> {isOpen && <div>Dropdown Content</div>} </div> ); }
// 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 ( <div> <p>The switch is {isOn ? 'ON' : 'OFF'}</p> <button onClick={toggle}>Toggle</button> </div> ); }
// useArray - Array manipulation hook function useArray<T>(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 ( <div> {todos.map((todo, index) => ( <div key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(index)} /> <span>{todo.text}</span> <button onClick={() => remove(index)}>Delete</button> </div> ))} </div> ); }
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
-
Rules of Hooks
-
useState Hook
-
useEffect Hook
-
useContext Hook
-
useReducer Hook
-
useCallback Hook
-
useMemo Hook
-
useRef Hook
-
useLayoutEffect Hook
-
useImperativeHandle Hook
Guides and Best Practices
-
Reusing Logic with Custom Hooks
-
Synchronizing with Effects
-
You Might Not Need an Effect
-
Separating Events from Effects
-
Removing Effect Dependencies
-
Lifecycle of Reactive Effects
TypeScript Resources
-
React TypeScript Cheatsheet
-
React TypeScript Hooks
-
Advanced React TypeScript
Additional Resources
-
usehooks.com - Custom Hooks Collection
-
usehooks-ts - TypeScript Hooks
-
React Hooks FAQ
-
ESLint Plugin React Hooks