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.
What is a Rerender?
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
The role of Reconciliation
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:
- Something changes — state, props, or context.
- React calls the component function again, producing new JSX.
- React builds a new Virtual DOM tree from that JSX.
- It compares the new tree with the previous one (this is called diffing).
- Finally, React updates only the changed parts in the real DOM.
So even though React updates only the changed DOM nodes, it still re-runs your component function every time something triggers a rerender.
Tools for optimizing rerenders
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 |
React.memo
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.
useMemo
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.
useCallback
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.
When do rerenders happen?
Rerenders can appear in a number of ways, specified below:
1. State Change
Tool: useMemo
Without optimization:
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>
);
}
With optimization:
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.
2. Props Change
Tool: React.memo
Without optimization:
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} />);
}
With optimization:
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.
3. Parent Change (parent re-renders but child props don’t change)
Tool: React.memo
Without optimization:
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>
);
}
With optimization:
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.
4. Context Change
Tool: useMemo
Without optimization:
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>;
}
With optimization:
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.
5. Unstable Callback Identity
Tool: useCallback
Without optimization:
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)"
/>
</>
);
}
With optimization:
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.
Quick mapping recap
- State change →
useMemo(cache heavy derived values). - Props change →
React.memo(skip when shallow-equal). - Parent change →
React.memo(child ignores parent re-renders). - Context change →
useMemo(provider value) (prevent global fan-out). - Unstable callbacks →
useCallback(stabilize handler identity for memoized children).
