When you get deeper into the world of React you can encoutner issues with code optimalization — rerenders. Rerenders happens when a component runs its render function again to update what appears on the screen.
Usually, rerenders are fine — React is fast — but in some cases (like rendering big lists, charts, or deeply nested components), optimizing them can make a visible difference.
A rerender occurs when a component runs its function again to produce new JSX. It doesn’t mean the DOM instantly updates — React first figures out what actually changed before touching the real DOM
Rerenders are tightly connected with a process called Reconciliation.
It’s a process when React figure out what actually changed in the UI after a rerender.
Every time a component rerenders, React builds a new virtual DOM tree. Then it compares this new tree with the previous one to determine what needs to be updated in the real DOM.
When a component rerenders:
So even though React updates only the changed DOM nodes, it still re-runs your component function every time something triggers a rerender.
React provides memoization tools to help control when components or values are recalculated.
| Tool | Protects | Description |
|---|---|---|
React.memo | Components | Skips re-render if props haven’t changed |
useMemo | Values | Caches computed values between renders |
useCallback | Functions | Caches function definitions between renders |
Wraps a component so it only rerenders when its props change.
import { memo } from 'react';
const Child = ({ value }) => {
console.log('Rendered');
return <div>{value}</div>;
};
export default memo(Child);
Now Child won’t rerender unless value changes.
Caches the result of an expensive calculation:
const sortedData = useMemo(() => {
return data.sort((a, b) => a.name.localeCompare(b.name));
}, [data]);
Without useMemo, the sorting would run on every render, even when data didn’t change.
With useMemo, React remembers the result of the previous calculation and only recomputes when one of the dependencies changes.
Keeps function references stable between renders:
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
Without useCallback, this function would be recreated every render — causing memoized children that receive it as a prop to rerender unnecessarily.
Rerenders can appear in a number of ways, specified below:
Tool: useMemo
import { useState } from 'react';
function CartTotal() {
const [items, setItems] = useState([
{ id: 1, name: 'Tea', price: 4, qty: 2 },
{ id: 2, name: 'Coffee', price: 6, qty: 1 },
]);
const [coupon, setCoupon] = useState('');
const total = items.reduce((sum, it) => {
// pretend heavy: taxes, shipping tiers, etc.
let line = 0;
for (let i = 0; i < 20000; i++) line = it.price * it.qty + i * 0.000001;
return sum + line;
}, 0);
return (
<section>
<h3>Cart Total: {total.toFixed(2)}</h3>
<input value={coupon} onChange={(e) => setCoupon(e.target.value)} placeholder="Coupon" />
<button
onClick={() =>
setItems((prev) => [...prev, { id: Date.now(), name: 'Mug', price: 8, qty: 1 }])
}>
Add Mug
</button>
</section>
);
}
import { useMemo, useState } from 'react';
function CartTotal() {
const [items, setItems] = useState([
{ id: 1, name: 'Tea', price: 4, qty: 2 },
{ id: 2, name: 'Coffee', price: 6, qty: 1 },
]);
const [coupon, setCoupon] = useState('');
const total = useMemo(() => {
return items.reduce((sum, it) => {
let line = 0;
for (let i = 0; i < 20000; i++) line = it.price * it.qty + i * 0.000001;
return sum + line;
}, 0);
}, [items]);
return (
<section>
<h3>Cart Total: {total.toFixed(2)}</h3>
<input value={coupon} onChange={(e) => setCoupon(e.target.value)} placeholder="Coupon" />
<button
onClick={() =>
setItems((prev) => [...prev, { id: Date.now(), name: 'Mug', price: 8, qty: 1 }])
}>
Add Mug
</button>
</section>
);
}
Tie heavy recomputation to the state that actually changes (items), not to unrelated state like coupon.
Tool: React.memo
function ProductRow({ product, onAddToCart }) {
return (
<div className="row">
<span>{product.name}</span>
<strong>${product.price}</strong>
<button onClick={() => onAddToCart(product.id)}>Add</button>
</div>
);
}
export default function ProductList({ products, onAddToCart }) {
// when parent re-renders (e.g., search elsewhere), every row re-renders
return products.map((p) => <ProductRow key={p.id} product={p} onAddToCart={onAddToCart} />);
}
import { memo } from 'react';
const ProductRow = memo(function ProductRow({ product, onAddToCart }) {
return (
<div className="row">
<span>{product.name}</span>
<strong>${product.price}</strong>
<button onClick={() => onAddToCart(product.id)}>Add</button>
</div>
);
});
export default function ProductList({ products, onAddToCart }) {
return products.map((p) => <ProductRow key={p.id} product={p} onAddToCart={onAddToCart} />);
}
If a row’s props are shallowly equal, React.memo skips re-rendering that row.
Tool: React.memo
import { useState } from "react";
function Header({ storeName }) {
return <h1>{storeName}</h1>;
}
export default function Layout() {
const [storeName] = useState("Acme Market");
const [search, setSearch] = useState(""); // typing here re-renders Layout
return (
<div>
<Header storeName={storeName} /> {/* props value never changes */}
<inputvalue={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search products"
/>
{/* ...results... */}
</div>
);
}
import { memo, useState } from 'react';
const Header = memo(function Header({ storeName }) {
return <h1>{storeName}</h1>;
});
export default function Layout() {
const [storeName] = useState('Acme Market');
const [search, setSearch] = useState('');
return (
<div>
<Header storeName={storeName} />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search products"
/>
</div>
);
}
The parent re-renders (search input), but the child’s prop (storeName) is identical. React.memo lets the header skip those parent-driven re-renders.
Tool: useMemo
import { createContext, useState, useContext } from 'react';
const AuthContext = createContext({ user: null, login: () => {}, logout: () => {} });
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// new object each render -> all consumers update on theme toggles too
const value = { user, login: () => setUser({ id: 1, name: 'Ava' }), logout: () => setUser(null) };
return (
<AuthContext.Provider value={value}>
<button onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
Toggle Theme
</button>
{children}
</AuthContext.Provider>
);
}
export function UserBadge() {
const { user } = useContext(AuthContext);
return <span>{user ? `Hi, ${user.name}` : 'Guest'}</span>;
}
import { createContext, useState, useMemo, useContext } from 'react';
const AuthContext = createContext({ user: null, login: () => {}, logout: () => {} });
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({
user,
login: () => setUser({ id: 1, name: 'Ava' }),
logout: () => setUser(null),
}),
[user]
);
return (
<AuthContext.Provider value={value}>
<button onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
Toggle Theme
</button>
{children}
</AuthContext.Provider>
);
}
export function UserBadge() {
const { user } = useContext(AuthContext);
return <span>{user ? `Hi, ${user.name}` : 'Guest'}</span>;
}
Context consumers update when the provider’s value reference changes. Memoizing it prevents unrelated provider state from cascading.
Tool: useCallback
import { useState, memo } from 'react';
const Quantity = memo(function Quantity({ value, onChange }) {
// will still re-render because onChange identity changes each parent render
return (
<div>
<button onClick={() => onChange(value - 1)}>-</button>
<span>{value}</span>
<button onClick={() => onChange(value + 1)}>+</button>
</div>
);
});
export default function CartLine() {
const [qty, setQty] = useState(1);
const [note, setNote] = useState('');
const handleQty = (next) => setQty(Math.max(1, next)); // new function each render
return (
<>
<Quantity value={qty} onChange={handleQty} />
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Gift note (optional)"
/>
</>
);
}
import { useState, useCallback, memo } from 'react';
const Quantity = memo(function Quantity({ value, onChange }) {
return (
<div>
<button onClick={() => onChange(value - 1)}>-</button>
<span>{value}</span>
<button onClick={() => onChange(value + 1)}>+</button>
</div>
);
});
export default function CartLine() {
const [qty, setQty] = useState(1);
const [note, setNote] = useState('');
// stable identity, so Quantity can truly skip re-renders when value is unchanged
const handleQty = useCallback(
(next) => setQty((prev) => Math.max(1, typeof next === 'function' ? next(prev) : next)),
[]
);
return (
<>
<Quantity value={qty} onChange={handleQty} />
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Gift note (optional)"
/>
</>
);
}
A memoized child receiving an unstable function prop still re-renders every time. useCallback makes the handler reference stable so React.memo can work as intended.
useMemo (cache heavy derived values).React.memo (skip when shallow-equal).React.memo (child ignores parent re-renders).useMemo (provider value) (prevent global fan-out).useCallback (stabilize handler identity for memoized children).