begiedz logo begiedz.dev
React

Memoization in React

Dariusz Begiedza
#frontend#webdev#react#memoization
React Logo

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:

  1. Something changes — state, props, or context.
  2. React calls the component function again, producing new JSX.
  3. React builds a new Virtual DOM tree from that JSX.
  4. It compares the new tree with the previous one (this is called diffing).
  5. 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.

ToolProtectsDescription
React.memoComponentsSkips re-render if props haven’t changed
useMemoValuesCaches computed values between renders
useCallbackFunctionsCaches 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

Back to blog