ð react-hooks-patterns
Use when React Hooks patterns including useState, useEffect, useContext, useMemo, useCallback, and custom hooks. Use for modern React development.
Overview
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