
React Hooks look simple at first. You call useState , update a value, and the screen changes. Then the real questions begin: Why did my effect run twice? Why does this callback see an old value? Should I use useMemo everywhere for performance? When is useReducer better than useSt
10 React Hooks Explained with Real-World Examples: 2026 Guide
React Hooks look simple at first. You call useState, update a value, and the screen changes.
Then the real questions begin:
Why did my Effect run twice?
Why does my callback see an old value?
Should I use useMemo everywhere?
When is useReducer better than useState?
How can I update the interface before the server responds?
The difficult part is not memorizing Hook syntax. It is understanding which problem each Hook solves.
This guide explains ten important React Hooks using practical examples instead of isolated demonstrations.
Before using any Hook, remember these two rules:
Call Hooks only at the top level of a component or custom Hook.
Call Hooks only from React components or custom Hooks.
Do not call Hooks inside conditions, loops, event handlers, or ordinary JavaScript functions.
React relies on Hooks being called in the same order during every render.
useState gives a component memory.
Use it when a value can change and that change should update what the user sees.
Imagine an e-commerce page where a customer selects the product quantity:
import { useState } from "react";
export default function QuantitySelector() {
const [quantity, setQuantity] = useState(1);
function increase() {
setQuantity(current => current + 1);
}
function decrease() {
setQuantity(current => Math.max(1, current - 1));
}
return (
<div>
<button onClick={decrease}>-</button>
<span>{quantity}</span>
<button onClick={increase}>+</button>
</div>
);
}The function form is important:
setQuantity(current => current + 1);It should be used when the next state depends on the previous state. It prevents problems when React batches multiple state updates.
React state should be treated as read-only.
// Wrong: Modifies the existing object
user.name = "Alaa";
setUser(user);Create a new object instead:
setUser(current => ({
...current,
name: "Alaa",
}));Simple rule: If changing the value should update the screen, it probably belongs in state.
useEffect is often described as code that runs after rendering.
A more useful definition is:
Use an Effect to synchronize your component with something outside React.
Examples include:
API requests
Timers
Browser events
WebSocket connections
Third-party libraries
Analytics services
Here is a product component that loads data from an API:
import { useEffect, useState } from "react";
export default function ProductDetails({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
async function loadProduct() {
setLoading(true);
try {
const response = await fetch(
`/api/products/${productId}`,
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error("Could not load the product");
}
const data = await response.json();
setProduct(data);
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
loadProduct();
return () => {
controller.abort();
};
}, [productId]);
if (loading) {
return <p>Loading...</p>;
}
if (!product) {
return <p>Product not found.</p>;
}
return <h1>{product.name}</h1>;
}When productId changes, React cancels the previous request and starts a new one.
In development, React Strict Mode may run an extra setup and cleanup cycle. This helps discover missing cleanup logic. It does not mean every Effect runs twice in production.
This Effect is unnecessary:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);The value can be calculated directly:
const fullName = `${firstName} ${lastName}`;Simple rule: If you are not synchronizing with an external system, you may not need an Effect.
Prop drilling happens when data passes through several components that do not need it just so a deeply nested component can receive it.
useContext allows a component to read a shared value from the nearest matching provider.
import {
createContext,
useContext,
useState,
} from "react";
const CurrencyContext = createContext(null);
export function StoreProvider({ children }) {
const [currency, setCurrency] = useState("USD");
return (
<CurrencyContext.Provider
value={{ currency, setCurrency }}
>
{children}
</CurrencyContext.Provider>
);
}
export function ProductPrice({ amount }) {
const context = useContext(CurrencyContext);
if (!context) {
throw new Error(
"ProductPrice must be used inside StoreProvider"
);
}
const { currency } = context;
return (
<span>
{new Intl.NumberFormat("en", {
style: "currency",
currency,
}).format(amount)}
</span>
);
}Context is useful for shared information such as:
The authenticated user
Theme
Language
Currency
Permissions
Feature flags
When a Context value changes, components consuming that Context may render again.
A single large Context containing unrelated information can create unnecessary rendering and tight coupling.
Simple rule: Use Context for information needed by many components at different levels. Do not use it as an automatic replacement for props or proper state management.
useState works well for independent values.
When several values change together according to specific rules, useReducer can make the logic easier to understand and test.
Here is a simple shopping cart reducer:
import { useReducer } from "react";
function cartReducer(state, action) {
switch (action.type) {
case "product_added": {
const existingProduct = state.find(
item => item.id === action.product.id
);
if (existingProduct) {
return state.map(item =>
item.id === action.product.id
? {
...item,
quantity: item.quantity + 1,
}
: item
);
}
return [
...state,
{
...action.product,
quantity: 1,
},
];
}
case "product_removed":
return state.filter(
item => item.id !== action.productId
);
case "cart_cleared":
return [];
default:
throw new Error(
`Unknown action: ${action.type}`
);
}
}
export default function Cart() {
const [items, dispatch] = useReducer(
cartReducer,
[]
);
function addKeyboard() {
dispatch({
type: "product_added",
product: {
id: 7,
name: "Keyboard",
price: 80,
},
});
}
return (
<div>
<button onClick={addKeyboard}>
Add keyboard
</button>
<button
onClick={() =>
dispatch({ type: "cart_cleared" })
}
>
Clear cart
</button>
<p>{items.length} different products</p>
</div>
);
}The component describes what happened, while the reducer determines how state changes.
Consider useReducer when:
State contains several related fields.
Multiple event handlers update the same state.
State transitions include business rules.
You want transitions represented by clear actions.
Simple rule: Use useState for simple values. Consider useReducer when the state transitions become difficult to follow.
useRef returns the same mutable object during every render.
Changing its current property does not cause the component to render again.
A common use is accessing a DOM element:
import { useRef } from "react";
export default function SearchBox() {
const inputRef = useRef(null);
function focusSearch() {
inputRef.current?.focus();
}
return (
<div>
<input
ref={inputRef}
placeholder="Search products"
/>
<button onClick={focusSearch}>
Focus search
</button>
</div>
);
}A ref can also store:
A timer ID
A previous value
An external library instance
Information needed by event handlers
A value that must survive renders
For example:
const timeoutRef = useRef(null);
function scheduleSearch(query) {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
searchProducts(query);
}, 400);
}Changing state causes React to render the component again.
Changing a ref does not cause another render.
Simple rule: If the value affects what the user sees, use state. If it is only needed by your code between renders, consider using a ref.
useMemo caches the result of a calculation until one of its dependencies changes.
Imagine a dashboard that filters and sorts thousands of orders:
import { useMemo } from "react";
export default function Orders({
orders,
search,
sortBy,
}) {
const visibleOrders = useMemo(() => {
const normalizedSearch =
search.toLowerCase();
return orders
.filter(order =>
order.customerName
.toLowerCase()
.includes(normalizedSearch)
)
.toSorted((first, second) => {
if (sortBy === "total") {
return second.total - first.total;
}
return (
new Date(second.createdAt) -
new Date(first.createdAt)
);
});
}, [orders, search, sortBy]);
return visibleOrders.map(order => (
<div key={order.id}>
{order.customerName}
</div>
));
}React recalculates visibleOrders only when orders, search, or sortBy changes.
This normally does not need useMemo:
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);The direct version is clearer:
const fullName = `${firstName} ${lastName}`;Memoization has its own cost and adds complexity.
Use it when:
A calculation is noticeably expensive.
Profiling reveals a performance problem.
Stable object identity is required by another optimization.
Simple rule: useMemo is a performance optimization, not something your code needs for correctness.
A component normally creates new function objects every time it renders. Usually, this is not a problem.
useCallback is useful when the function’s identity matters.
One example is passing a callback to a component wrapped with React.memo.
import {
memo,
useCallback,
useState,
} from "react";
const ProductRow = memo(function ProductRow({
product,
onAdd,
}) {
console.log("Rendering", product.name);
return (
<button onClick={() => onAdd(product.id)}>
Add {product.name}
</button>
);
});
export default function ProductList({
products,
}) {
const [cart, setCart] = useState([]);
const addToCart = useCallback(productId => {
setCart(current => [
...current,
productId,
]);
}, []);
return products.map(product => (
<ProductRow
key={product.id}
product={product}
onAdd={addToCart}
/>
));
}Because addToCart keeps the same function reference, ProductRow can skip unnecessary renders when its other properties have not changed.
useMemo caches a calculated value.
useCallback caches a function reference.
Simple rule: Do not wrap every function in useCallback. Use it when stable function identity solves an actual rendering or dependency problem.
Some updates are urgent.
For example, an input should update immediately while the user is typing. Rendering a large result list is less urgent.
useTransition allows you to mark an update as non-urgent.
import {
useState,
useTransition,
} from "react";
export default function ProductSearch() {
const [input, setInput] = useState("");
const [query, setQuery] = useState("");
const [isPending, startTransition] =
useTransition();
function handleChange(event) {
const value = event.target.value;
setInput(value);
startTransition(() => {
setQuery(value);
});
}
return (
<div>
<input
value={input}
onChange={handleChange}
/>
{isPending && (
<small>Updating results...</small>
)}
<LargeProductList query={query} />
</div>
);
}Updating the input is urgent, so setInput runs normally.
Updating the large result list is less urgent, so setQuery runs inside a transition.
A transition does not use a timer, and it does not make JavaScript run on another thread.
It tells React that the update can be interrupted if something more important happens.
Simple rule: Use a transition when a slow render makes an important interaction feel blocked.
useDeferredValue provides a deferred version of a value.
It is useful when a value changes quickly, but an expensive part of the interface does not need to update immediately.
import {
useDeferredValue,
useState,
} from "react";
export default function CustomerSearch({
customers,
}) {
const [query, setQuery] = useState("");
const deferredQuery =
useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = customers.filter(customer =>
customer.name
.toLowerCase()
.includes(deferredQuery.toLowerCase())
);
return (
<div>
<input
value={query}
onChange={event =>
setQuery(event.target.value)
}
placeholder="Search customers"
/>
<div
style={{
opacity: isStale ? 0.6 : 1,
}}
>
{results.map(customer => (
<p key={customer.id}>
{customer.name}
</p>
))}
</div>
</div>
);
}The input shows the newest value immediately. The customer list may temporarily use the previous value while React prepares the new result.
This is not the same as debouncing.
Debouncing waits for a specific amount of time. A deferred value allows React to schedule the update based on rendering priority.
Simple rule: Use useDeferredValue when a rapidly changing value controls a slower part of the interface.
Users should not always have to wait for a network request before seeing the expected result.
useOptimistic lets the interface temporarily show the expected result while an operation is being processed.
Here is an optimistic comment form:
import { useOptimistic } from "react";
export default function Comments({
comments,
saveComment,
}) {
const [
optimisticComments,
addOptimisticComment,
] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{
...newComment,
pending: true,
},
]
);
async function submitComment(formData) {
const text = formData.get("comment");
const temporaryComment = {
id: crypto.randomUUID(),
text,
};
addOptimisticComment(
temporaryComment
);
await saveComment(text);
}
return (
<div>
<form action={submitComment}>
<input name="comment" required />
<button type="submit">Post</button>
</form>
{optimisticComments.map(comment => (
<p
key={comment.id}
style={{
opacity: comment.pending
? 0.5
: 1,
}}
>
{comment.text}
{comment.pending &&
" (Sending...)"}
</p>
))}
</div>
);
}The new comment appears immediately with reduced opacity.
When the server operation finishes, the real data replaces the temporary optimistic state.
Optimistic updates are suitable for operations likely to succeed, such as:
Liking a post
Adding a comment
Updating a preference
Adding an item to a list
The application should still display an appropriate error if the operation fails.
Simple rule: Use optimistic updates to make reliable actions feel immediate, while keeping the server as the final source of truth.
useState: Stores values that affect the interface.
useEffect:
No approved comments are visible yet. New community replies may wait for moderation.