Why Hooks Replaced Classes
Before React 16.8, the only way to use state or lifecycle methods was with class components. That meant writing constructor(), binding this in event handlers, scattering related logic across componentDidMount, componentDidUpdate, and componentWillUnmount, and wrapping everything in render props or higher-order components to share logic.
Hooks fixed all of that. A single useEffect replaces three lifecycle methods. State lives right next to the JSX that uses it. Shared logic goes into custom hooks instead of HOCs. And function components are shorter, easier to read, and easier for tools to optimize.
As of 2026, the React team considers class components legacy. React 19 and the React Compiler are built entirely around function components and hooks. If you are writing class components in new code, you are fighting the framework.
Hooks are not just an alternative to classes — they are the foundation of modern React. Server Components, the React Compiler, Suspense, and concurrent features all assume function components with hooks.
useState: Managing Component State
useState is the most fundamental hook. It declares a state variable, gives you its current value, and provides a function to update it. Each call to the setter triggers a re-render.
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 updater form. This avoids bugs with stale closures, especially in async code or effects.
// Wrong: may use stale value in rapid clicks or async operations
setCount(count + 1);
// Correct: always uses the latest state
setCount(prev => prev + 1);
State with Objects
Unlike this.setState in class components, useState does not merge objects automatically. You must spread the previous state yourself.
const [user, setUser] = useState({ name: '', email: '', role: 'viewer' });
// Update one field while keeping the rest
setUser(prev => ({ ...prev, name: 'Alice' }));
Lazy Initialization
If computing the initial state is expensive (parsing JSON, reading localStorage), pass a function to useState. It runs only on the first render.
// This runs on EVERY render (bad)
const [data, setData] = useState(JSON.parse(localStorage.getItem('data')));
// This runs only on the FIRST render (good)
const [data, setData] = useState(() => JSON.parse(localStorage.getItem('data')));
When working with complex state that involves JSON payloads, the JSON Formatter can help you inspect and debug your state objects during development.
useEffect: Side Effects and Lifecycle
useEffect runs side effects after React renders your component. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount with a single, composable API.
import { useEffect } from 'react';
// Run after EVERY render
useEffect(() => {
document.title = `Count: ${count}`;
});
// Run only on mount (empty dependency array)
useEffect(() => {
fetchUserData();
}, []);
// Run when specific values change
useEffect(() => {
fetchUserData(userId);
}, [userId]);
// Cleanup (runs before re-execution and on unmount)
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = handleMessage;
return () => {
ws.close(); // Cleanup: close connection
};
}, [url]);
The Dependency Array
The dependency array tells React when to re-run your effect. If you omit it, the effect runs after every render. If you pass an empty array, it runs once on mount. If you list specific values, it runs when any of those values change (compared with Object.is).
Never lie about dependencies to "fix" infinite loops. If your effect uses a value, it must be in the dependency array. Use eslint-plugin-react-hooks to catch missing dependencies automatically. If you have a legitimate reason to skip a dependency, restructure the effect instead.
Data Fetching Pattern
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // Prevent state updates after unmount
async function fetchUser() {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) {
setUser(data);
setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId]);
if (loading) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
The cancelled flag prevents the classic "state update on unmounted component" warning. When userId changes, the cleanup function sets cancelled = true for the old fetch, and a new fetch starts for the new user.
useRef: DOM Access and Mutable Values
useRef returns a mutable object with a .current property that persists across renders without causing re-renders when changed. It has two primary uses: referencing DOM elements and storing mutable values.
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Auto-focus on mount
}, []);
return <input ref={inputRef} placeholder="Search..." />;
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current; // Returns the value from the previous render
}
// Usage
function PriceDisplay({ price }) {
const previousPrice = usePrevious(price);
const direction = price > previousPrice ? 'up' : 'down';
return <span className={direction}>${price}</span>;
}
useRef vs useState: When to Use Which
| Scenario | Use |
|---|---|
| Value change should update the UI | useState |
| Store a DOM element reference | useRef |
| Store a timer/interval ID | useRef |
| Track if component is mounted | useRef |
| Form input value (controlled) | useState |
| Cache previous prop value | useRef |
useContext: Shared State Without Prop Drilling
When multiple components need the same data and it is cumbersome to pass props through every level of the tree, useContext lets any descendant component read the value directly.
import { createContext, useContext, useState } from 'react';
// 1. Create the context
const ThemeContext = createContext('light');
// 2. Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Consume with useContext (any depth)
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Current: {theme}
</button>
);
}
Every component that calls useContext(SomeContext) re-renders when the context value changes — even if it only uses one property of the value object. For frequently-updating contexts, split them (one for state, one for dispatch) or use an external state library like Zustand.
useMemo and useCallback: Performance
These two hooks are memoization tools. They prevent expensive recalculations or unnecessary re-renders by caching values between renders.
useMemo: Cache Computed Values
function ProductList({ products, filter }) {
// Only re-filters when products or filter changes
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === filter);
}, [products, filter]);
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
useCallback: Cache Functions
const ExpensiveChild = React.memo(function ExpensiveChild({ onClick, data }) {
console.log('ExpensiveChild rendered');
return <button onClick={onClick}>{data.label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [label, setLabel] = useState('Click me');
// Without useCallback, a new function is created on every render,
// which defeats React.memo on ExpensiveChild
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // No dependencies: function never changes
const data = useMemo(() => ({ label }), [label]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ExpensiveChild onClick={handleClick} data={data} />
</div>
);
}
In the example above, incrementing count re-renders Parent but does not re-render ExpensiveChild because both handleClick and data maintain stable references.
The React Compiler (shipping with React 19) can automatically memoize values and functions at compile time. If you are on React 19+, you may not need manual useMemo and useCallback as often. However, understanding how they work is still essential for debugging and for codebases not yet using the compiler.
useReducer: Complex State Logic
When state transitions become complex — multiple actions, conditional logic, or deeply nested updates — useReducer provides a structured alternative to useState.
import { useReducer } from 'react';
const initialState = { items: [], loading: false, error: null };
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.payload) };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'CLEAR':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item });
const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id });
return (
<div>
{state.items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
</div>
);
}
The reducer pattern keeps all state transitions in one place, making them testable outside the component. The dispatch function is also stable across renders, so you can pass it through context without causing unnecessary re-renders.
If your reducer starts handling API response data, validate the shape with the JSON Schema Validator to catch malformed payloads early.
Custom Hooks: Reusable Logic
A custom hook is a function whose name starts with use and that calls other hooks inside it. Custom hooks let you extract and share stateful logic between components without changing the component tree.
useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, 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 [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage: search input that waits 300ms after typing
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
setLoading(true);
setError(null);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchData();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
When converting HTML templates into React components, the HTML to JSX Converter handles the syntax transformation automatically — converting class to className, self-closing tags, and inline styles to objects.
Common Mistakes and How to Fix Them
1. Missing Dependency Array in useEffect
// Missing dependency array = runs after EVERY render
useEffect(() => {
fetchData(); // This fires on every single re-render
});
// Fix: add the dependency array
useEffect(() => {
fetchData();
}, []); // Runs once on mount
2. Object/Array as Dependency
function App({ userId }) {
const options = { method: 'GET', headers: { auth: token } }; // New object every render
useEffect(() => {
fetch(`/api/users/${userId}`, options);
}, [userId, options]); // options is new every time = infinite loop
// Fix: memoize the object
const options = useMemo(() => ({
method: 'GET', headers: { auth: token }
}), [token]);
}
3. Stale Closures
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // Bug: count is always 0 (stale closure)
}, 1000);
return () => clearInterval(id);
}, []);
// Fix: use functional updater
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // Always uses latest value
}, 1000);
return () => clearInterval(id);
}, []);
}
4. Calling Hooks Conditionally
// WRONG: hooks must not be inside conditions
function Profile({ isLoggedIn }) {
if (!isLoggedIn) return <p>Please log in</p>;
const [user, setUser] = useState(null); // Hook order changes!
// FIX: always call hooks at the top level
const [user, setUser] = useState(null);
if (!isLoggedIn) return <p>Please log in</p>;
}
5. Over-Memoizing
Wrapping everything in useMemo and useCallback does not make your app faster. Each hook has overhead (storing the cached value, comparing dependencies). Only memoize when you have measured a performance problem or when a stable reference is required for correctness (e.g., preventing infinite useEffect loops).
Use the JavaScript Formatter to keep your hook code clean and consistently indented while working through refactors like these.
The Rules of Hooks
React enforces two rules for hooks. Breaking them causes bugs that are difficult to trace.
- Only call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. React relies on call order to match hooks to their state between renders. If the order changes, state gets mixed up.
- Only call hooks from React functions. Call hooks from function components or from custom hooks. Do not call them from regular JavaScript functions, class components, or event handlers.
The eslint-plugin-react-hooks package enforces both rules automatically. Add it to your ESLint configuration — the ESLint Config Generator can help you set up a project-ready config with React rules included.
State: useState, useReducer. Context: useContext. Refs: useRef, useImperativeHandle. Effects: useEffect, useLayoutEffect, useInsertionEffect. Performance: useMemo, useCallback, useTransition, useDeferredValue. Other: useId, useSyncExternalStore, useDebugValue, useActionState, useFormStatus, useOptimistic, use.
Format, Convert, and Validate Your Code
QTool has 269 free developer tools for JavaScript, TypeScript, JSON, and more. Format code, convert between formats, and validate schemas — all in your browser.
Open JS Formatter HTML to JSXReact Developer Tools
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 (constructor, this binding, render methods), 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(() => expensiveCalculation(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 when you have an expensive calculation you want to skip on re-renders. Use useCallback when you need a stable function reference, typically when passing callbacks to child components wrapped in React.memo to prevent unnecessary re-renders.
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 (inputRef.current.focus()), storing previous values, holding interval or timeout IDs, and tracking whether a component is mounted. Use useState when changing the value should update the UI. The key difference is that updating a ref (ref.current = newValue) does not cause a re-render, while calling a state setter (setValue(newValue)) does. If you need the screen to reflect the change, use state. If you just need to remember something silently, use a ref.
Infinite loops happen when your useEffect updates a value that is also in its dependency array. For example, if your effect calls setState(count + 1) and count is in the dependency array, the state change triggers a re-render, which triggers the effect again, creating an infinite loop. To fix this: use the functional updater form setState(prev => prev + 1) so you do not need count in the dependency array; move objects and arrays that are recreated on every render outside the component or wrap them with useMemo; use a ref instead of state if you do not need to trigger a re-render; and always specify the correct dependency array (never omit it to hide the problem). The ESLint plugin eslint-plugin-react-hooks catches most of these issues at development time.
No. useCallback adds overhead (the hook call itself, the dependency comparison, and the cached reference). It only provides a benefit when the stable function reference actually prevents work, specifically when the function is passed as a prop to a child component 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 and not passed down, useCallback adds complexity without benefit. The React team recommends writing code without useCallback first, then adding it only where you measure an actual performance problem. The React Compiler (available since React 19) can automatically memoize functions, reducing the need for manual useCallback in many cases.
Use useContext combined with a context provider. Create a context with React.createContext(), wrap your component tree in a Provider that holds the shared state, and call useContext() in any descendant component to access that state. For simple global state (theme, auth, locale), this is sufficient. For complex state with frequent updates, consider splitting contexts (one for state, one for dispatch) to avoid re-rendering every consumer on every update. For application-scale state management with many consumers, external libraries like Zustand, Jotai, or Redux Toolkit may perform better because they allow components to subscribe to specific slices of state rather than the entire context value.