React Hooks changed how developers build user interfaces. Since their introduction in React 16.8, hooks have become the standard way to manage state, handle side effects, and share logic between components. Class components still work, but the React team has been clear: function components with hooks are the path forward.
This guide covers every hook you will encounter in a modern React codebase, from the foundational useState and useEffect to React 19 additions like use() and useOptimistic. Each section includes code you can copy into a project and start using immediately.
Format your React code. The JavaScript Formatter auto-formats JSX and JavaScript with consistent indentation and spacing. Paste, format, copy.
What Are React Hooks?
Hooks are regular JavaScript functions that let you "hook into" React features from function components. They follow two rules:
- Only call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. React relies on the call order being the same on every render to correctly associate state with each hook call.
- Only call hooks from React functions. Call hooks from function components or from other custom hooks. Do not call them from regular JavaScript functions.
These rules exist because React tracks hooks by their position in the call sequence. If you put a useState inside an if block, the call sequence changes between renders, and React cannot match state to the correct hook.
The eslint-plugin-react-hooks package enforces both rules automatically. If you are using Create React App, Next.js, or Vite with the React plugin, it is already configured.
useState: Managing Component State
useState is the most fundamental hook. It declares a state variable and gives you a function to update it.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Functional Updates
When the new state depends on the previous state, use the functional form. This avoids stale closures when multiple updates happen in the same render cycle.
// Correct: functional update
setCount(prev => prev + 1);
// Risky: might use stale value in async handlers
setCount(count + 1);
Objects and Arrays as State
React uses Object.is for equality checks. If you mutate an object and pass the same reference, React skips the re-render. Always create a new object or array.
const [user, setUser] = useState({ name: 'Jane', role: 'dev' });
// Correct: spread to create new object
setUser(prev => ({ ...prev, role: 'admin' }));
// Wrong: mutation (React won't re-render)
user.role = 'admin';
setUser(user);
Lazy Initialization
If computing the initial value is expensive, pass a function instead of a value. React calls it only on the first render.
// This reads localStorage on every render (wasteful)
const [theme, setTheme] = useState(localStorage.getItem('theme'));
// This reads localStorage only once
const [theme, setTheme] = useState(() => localStorage.getItem('theme'));
useEffect: Side Effects and Lifecycle
useEffect runs code after the component renders. It handles data fetching, subscriptions, DOM manipulation, and anything else that interacts with the world outside of rendering.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
}
fetchUser();
// Cleanup: runs before re-running or unmounting
return () => { cancelled = true; };
}, [userId]); // Only re-run when userId changes
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
The Dependency Array
- No array: Effect runs after every render. Rarely what you want.
- Empty array
[]: Effect runs once on mount and cleanup runs on unmount. Equivalent tocomponentDidMount+componentWillUnmount. - With dependencies
[a, b]: Effect runs whenaorbchanges (shallow comparison withObject.is).
Cleanup Functions
Return a function from useEffect to clean up subscriptions, timers, or abort controllers. React calls the cleanup before running the effect again and when the component unmounts.
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, []);
In React 19+, the recommended pattern for data fetching is using the use() hook with Suspense boundaries rather than useEffect. useEffect remains the right tool for subscriptions, event listeners, and DOM side effects.
useContext: Sharing State Without Props
useContext reads a value from a React context, eliminating the need to pass props through intermediate components (prop drilling).
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
When the context value changes, every component that calls useContext for that context re-renders. For frequently-changing values, consider splitting into multiple contexts or using external state libraries like Zustand or Jotai that allow subscribing to specific slices.
useReducer: Complex State Logic
useReducer is an alternative to useState for state that involves multiple sub-values or when the next state depends on the previous state in complex ways.
import { useReducer } from 'react';
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function StepCounter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count} (step: {state.step})</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
/>
</div>
);
}
A practical rule: if your useState updater functions are getting complex or you have multiple related state variables that change together, switch to useReducer. The reducer centralizes state transitions in one testable function.
useRef: Refs and Mutable Values
useRef creates a mutable object whose .current property persists across renders without triggering re-renders when changed.
DOM References
import { useRef } from 'react';
function SearchInput() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}
Storing Previous Values
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function PriceDisplay({ price }) {
const prevPrice = usePrevious(price);
const direction = price > prevPrice ? 'up' : price < prevPrice ? 'down' : '';
return <span className={direction}>${price}</span>;
}
Storing Interval IDs
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Performance Hooks: useMemo and useCallback
useMemo
useMemo caches the result of a computation. It recalculates only when its dependencies change.
import { useMemo } from 'react';
function ProductList({ products, filterTerm }) {
const filtered = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(filterTerm.toLowerCase())
);
}, [products, filterTerm]);
return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
useCallback
useCallback caches a function definition. It returns the same function reference as long as the dependencies have not changed. This is useful when passing callbacks to memoized child components.
import { useCallback, memo } from 'react';
const ExpensiveChild = memo(function ExpensiveChild({ onClick, label }) {
console.log(`Rendering: ${label}`);
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, this creates a new function on every render,
// causing ExpensiveChild to re-render every time
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ExpensiveChild onClick={handleClick} label="Action" />
</div>
);
}
Do not optimize prematurely. Both useMemo and useCallback add overhead. Use them when you measure a performance problem, not on every value and function. The React Compiler in React 19+ can auto-memoize in many cases, making manual memoization less necessary.
React 19 Hooks: use, useOptimistic, useFormStatus
use()
The use hook reads a resource (a Promise or a context) during render. Unlike useEffect, it integrates with Suspense for a declarative loading pattern.
import { use, Suspense } from 'react';
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
function App() {
const [userId, setUserId] = useState(1);
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<p>Loading...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
use() is unique among hooks because it can be called inside conditionals and loops. It suspends the component until the Promise resolves, and React shows the nearest Suspense fallback while waiting.
useOptimistic
Shows an optimistic UI update while an async action is in progress. If the action fails, React automatically reverts to the previous state.
import { useOptimistic } from 'react';
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, { ...newTodo, pending: true }]
);
async function handleSubmit(formData) {
const title = formData.get('title');
addOptimistic({ id: Date.now(), title });
await addTodoAction(title);
}
return (
<div>
{optimisticTodos.map(todo => (
<p key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</p>
))}
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Add</button>
</form>
</div>
);
}
useFormStatus
Provides the status of a parent form to child components. This avoids prop drilling for form submission state.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
function ContactForm({ submitAction }) {
return (
<form action={submitAction}>
<input name="email" type="email" required />
<textarea name="message" required />
<SubmitButton />
</form>
);
}
For validating your JSX output or converting between HTML and JSX during development, the HTML to JSX Converter handles the transformation instantly.
Building Custom Hooks
Custom hooks extract reusable logic into a function. The only rule: the function name must start with use.
useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'dark');
return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
useDebounce
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage: debounce a search query
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`).then(/* ... */);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
useMediaQuery
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Usage
function Layout() {
const isMobile = useMediaQuery('(max-width: 768px)');
return isMobile ? <MobileNav /> : <DesktopNav />;
}
Common Mistakes and How to Fix Them
1. Missing Dependencies in useEffect
// Bug: stale closure, count is always 0
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
// Fix: functional updater
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
2. Creating Objects in the Dependency Array
// Bug: options is a new object every render, effect runs infinitely
function App() {
const options = { method: 'GET' };
useEffect(() => {
fetch('/api/data', options);
}, [options]); // new reference every render!
// Fix: useMemo or move inside effect
const options = useMemo(() => ({ method: 'GET' }), []);
}
3. Mutating State Directly
// Bug: mutation, React doesn't detect the change
const [items, setItems] = useState([1, 2, 3]);
items.push(4);
setItems(items); // same reference, no re-render
// Fix: create a new array
setItems(prev => [...prev, 4]);
4. Forgetting Cleanup in useEffect
// Bug: memory leak if component unmounts during fetch
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
// Fix: abort controller
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(() => {});
return () => controller.abort();
}, []);
5. Using useEffect for Derived State
// Anti-pattern: useEffect to sync derived state
const [firstName, setFirstName] = useState('Jane');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Fix: just compute it during render
const fullName = `${firstName} ${lastName}`;
// Or useMemo if the computation is expensive
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
The ESLint Config Generator can scaffold an ESLint configuration with the React hooks plugin enabled, catching many of these mistakes at development time.
Related Developer Tools
Free browser-based tools to complement your React development workflow.
Frequently Asked Questions
React Hooks are functions that let you use state, lifecycle behavior, context, and other React features inside function components without writing classes. Introduced in React 16.8, hooks are now the standard way to build React applications. They simplify code by eliminating class boilerplate, make logic reusable through custom hooks, and compose better than higher-order components or render props. As of 2026, the React team recommends function components with hooks for all new code, and class components are considered legacy.
useMemo caches the result of a computation: useMemo(() => expensiveCalc(a, b), [a, b]) returns the cached value and only recalculates when a or b changes. useCallback caches the function itself: useCallback((x) => x + a, [a]) returns the same function reference as long as a stays the same. In practice, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo for expensive calculations you want to skip on re-renders. Use useCallback for stable function references, typically when passing callbacks to child components wrapped in React.memo.
Infinite loops happen when your useEffect updates a value that is in its own dependency array. For example, if the effect calls setState(count + 1) and count is a dependency, the state change triggers a re-render, which triggers the effect again. To fix this: use the functional updater form setState(prev => prev + 1) so you do not need count in the dependency array; wrap objects and arrays with useMemo so they have stable references; use a ref instead of state if you do not need to trigger a re-render; and always specify the correct dependency array. The eslint-plugin-react-hooks catches most of these at development time.
Use useRef when you need to store a value that persists across renders but should not trigger a re-render when it changes. Common use cases include referencing DOM elements for focus or measurement, storing previous values, holding interval or timeout IDs, and tracking mounted state. Use useState when changing the value should update the UI. The key difference: updating a ref (ref.current = newValue) is silent, while calling a state setter (setValue(newValue)) triggers a re-render. If the screen needs to reflect the change, use state. If you just need to remember something, use a ref.
No. useCallback adds overhead: the hook call itself, the dependency comparison, and the cached reference. It only helps when the stable function reference prevents unnecessary work, specifically when the function is passed as a prop to a child wrapped in React.memo, or when it appears in the dependency array of another hook. If a function is only used inside the same component, useCallback adds complexity with no benefit. Write code without useCallback first, then add it where you measure a real performance problem. The React Compiler in React 19+ can automatically memoize in many cases, reducing the need for manual useCallback.
React 19 introduced several new hooks. use() reads resources like Promises and context during render, replacing useEffect for data fetching when used with Suspense. useFormStatus() gives child components access to the status of a parent form (pending, data, method, action) without prop drilling. useFormState() connects a form action to state that updates when the form is submitted. useOptimistic() lets you show an optimistic UI state while an async action is in progress, automatically reverting if it fails. React 19 also includes the React Compiler which can auto-memoize, reducing the need for manual useMemo and useCallback.