DEV Community

Cover image for React 19 useMemo Explained How to Make React Remember Stuff (and When Not To)
Ali Aslam
Ali Aslam

Posted on

React 19 useMemo Explained How to Make React Remember Stuff (and When Not To)

React re-renders your components a lot.
Most of the time, that’s totally fine — React is fast, browsers are fast, life is good.

But sometimes… a single line of code inside your component becomes the bottleneck.
It’s not obvious. It’s not broken.
It’s just quietly doing way more work than it needs to, over and over, every time React decides to render.

That’s where useMemo comes in.
It’s your way of saying:

“Hey React — I already did this work. If nothing important has changed, just give me the same result as last time.”

In this article, we’re going to break down exactly how useMemo works, when it actually helps, and when it’s just extra baggage in your code.


Table of Contents

Understanding the “Why” Behind useMemo

Building the Mental Model

First Steps with the API

Knowing When (and When Not) to Use It

Solving the “Why Did This Child Re-render?” Mystery

The Optimization Trio

Practical Patterns You’ll Actually Use

Don’t Guess: Measure First

— Pitfalls to Avoid

— Wrap-up & Mental Checklist


Section 1 — Why useMemo Exists (and When You Don’t Need It)

Picture this:
You’ve built a product page with a huge table — 10,000+ rows of data.
It looks awesome.

But when you change any filter, any sort option, or even update an unrelated bit of state…
…the UI freezes for half a second. 😬


The culprit?

Every render, your component sorts and filters all 10,000 rows from scratch.
Not just when the filter changes — on every render.
Even if you just toggled a “dark mode” switch that has nothing to do with the data, React still recomputes the entire sorted/filtered array.

This is not the same as a “too many keystrokes” problem that you’d fix with a debounce.
Here, the slowdown is caused by heavy calculations running too often — not from events firing too quickly.


Enter useMemo

useMemo is how you tell React:

“This calculation depends on X and Y.
If X and Y haven’t changed, don’t bother running it again — just give me the last result.”

It’s basically a cache scoped to your component for pure computations.
(Pure means: same inputs → same output, no side effects.)


Without vs. With useMemo

Without:

function ProductTable({ products, sortBy, filter }) {
  const processed = processData(products, sortBy, filter); // expensive

  return <Table data={processed} />;
}
Enter fullscreen mode Exit fullscreen mode

Even if only the theme changes, processData runs again.


With:

function ProductTable({ products, sortBy, filter }) {
  const processed = React.useMemo(() => {
    return processData(products, sortBy, filter);
  }, [products, sortBy, filter]);

  return <Table data={processed} />;
}
Enter fullscreen mode Exit fullscreen mode

Now it only reprocesses when the actual inputs change — no wasted work.


But… do you even need it?

useMemo has overhead too. You don’t slap it on every computation.
It’s worth using if:

  • The computation is actually expensive (big loops, heavy transformations).
  • The component re-renders frequently for reasons unrelated to that computation.

It’s not worth it if:

  • The computation is trivial (string concatenation, a quick filter on 5 items).
  • The component rarely re-renders.

Think of it like a “do not disturb” sign for calculations — great when needed, pointless otherwise.


Good reason: Heavy data processing or building large objects on every render.
Bad reason: “I saw it in a blog post, so I wrapped everything in it.”


Section 2 — The Mental Model

Think of useMemo as giving your component its own little vault.
Inside that vault, React stores:

  1. The last inputs you gave it (dependencies).
  2. The last result it computed from those inputs.

The deal is simple:

  • On the next render, React checks your dependencies.
  • If none of them changed, it just hands you the value from the vault. (Boom — instant, no recalculation.)
  • If any dependency changed, it re-runs your function, updates the vault, and hands you the new result.

Visualizing it

Let’s say you write:

const sorted = useMemo(() => sortItems(items), [items]);
Enter fullscreen mode Exit fullscreen mode
Render items changed? What happens?
#1 (first render) sortItems runs, vault stores result
#2 No React returns cached result
#3 Yes sortItems runs again, vault updates
#4 No Cached result returned again

Why this matters

React re-runs your component function every render — that’s normal.
But useMemo lets you say:

“This part of my component’s work is expensive.
Only redo it if these specific inputs change.”

It’s like having a smart assistant who remembers:

  • When they last did the task.
  • What they were given when they did it.

The two golden rules

  1. Pure only — The function you give useMemo must be pure (no side effects). React might throw away the cached value and re-run it later for any reason.
  2. Dependency accuracy — If the output depends on a variable, that variable must be in the dependencies array. (Otherwise, you risk stale values.)

Section 3 — The API & First Examples

At its core, useMemo is dead simple.

const memoizedValue = useMemo(
  () => {
    // some expensive calculation
    return result;
  },
  [dep1, dep2]
);
Enter fullscreen mode Exit fullscreen mode

Let’s break that down:

  1. useMemo — The hook itself.
  2. First argument → A function that returns the value you want to cache.
  3. Second argument → An array of dependencies. If any of them change between renders, React re-runs the function. If none change, React reuses the cached value.

Example: Sorting products

Without useMemo:

function ProductList({ products }) {
  const sorted = [...products].sort((a, b) => a.name.localeCompare(b.name));
  return <List items={sorted} />;
}
Enter fullscreen mode Exit fullscreen mode

Even if products hasn’t changed, you re-sort on every render.


With useMemo:

function ProductList({ products }) {
  const sorted = useMemo(() => {
    console.log("Sorting...");
    return [...products].sort((a, b) => a.name.localeCompare(b.name));
  }, [products]);

  return <List items={sorted} />;
}
Enter fullscreen mode Exit fullscreen mode

Now:

  • First render → runs the sort.
  • If products hasn’t changed → uses cached sorted array.

Try adding a console log — you’ll see the difference.


Example: Heavy math

function Fibonacci({ n }) {
  const fib = useMemo(() => {
    console.log("Calculating fib...");
    return slowFibonacci(n);
  }, [n]);

  return <div>{fib}</div>;
}
Enter fullscreen mode Exit fullscreen mode
  • If n stays the same, the expensive slowFibonacci never runs again.
  • Change n, and it recalculates.

Dependency Array Rules (This Trips Beginners)

  • Include everything your calculation depends on.
  • Don’t try to “cheat” by leaving something out — you’ll get stale values.
  • ESLint’s react-hooks/exhaustive-deps rule helps catch mistakes.

Pure Only — No Side Effects!

This function should:

  • Take inputs from dependencies.
  • Return a value.
  • Not do any data fetching, DOM changes, or state updates.

If you need side effects, that’s useEffect’s job, not useMemo’s.


Section 4 — When useMemo Helps vs. When It’s Useless (or Harmful)

If you hang around dev forums long enough, you’ll hear two extremes:

  • Camp A: “Wrap everything in useMemo!”
  • Camp B: “Never use useMemo!”

The truth?
It’s a tool. Use it when it solves a real problem, skip it when it doesn’t.


✅ When It Helps

1. Expensive Computations

If you’re sorting/filtering big lists, running heavy math, or generating huge derived objects — useMemo prevents doing that on every render.

Example:

const visibleRows = useMemo(
  () => bigList.filter(isVisible),
  [bigList, isVisible]
);
Enter fullscreen mode Exit fullscreen mode

2. Referential Stability

React’s re-render system compares references for objects, arrays, and functions.
If you pass a new object to a child each time, it triggers re-renders — even if the content is identical.

useMemo can keep the same reference as long as the contents haven’t changed:

const tableConfig = useMemo(
  () => ({ columns, sortable: true }),
  [columns]
);
Enter fullscreen mode Exit fullscreen mode

Great when you use React.memo on a child and want to avoid unnecessary re-renders.


3. Derived State

Sometimes your component calculates a value from props or other state.
If it’s heavy, memoize it instead of recalculating on every change.


❌ When It’s Useless (or Harmful)

1. Trivial Calculations

const double = value * 2; // ✅ no need for useMemo
Enter fullscreen mode Exit fullscreen mode

React can do this faster than it can check dependencies.


2. Rarely Re-rendered Components

If your component only renders when its props change anyway, caching won’t help much.


3. Premature Optimization

Adding useMemo everywhere makes your code harder to read and maintain — for zero gain.
Plus, useMemo itself has overhead. You’re trading one small cost for another.


The Litmus Test

Ask yourself:

  1. Is this calculation noticeably expensive?
  2. Does this component re-render often for reasons unrelated to that calculation?
  3. Will caching the result actually avoid wasted work?

If you can’t answer “yes” to #1 and either #2 or #3 — skip it.


Section 5 — Referential Equality & Child Re-renders

Let’s talk about a subtle but very real problem that happens in React all the time…
You optimize a child component with React.memo, but it still re-renders every time the parent does. 🤨


The Culprit: New References Every Time

In JavaScript:

  • Two primitives (42, 'hello') are equal if they have the same value.
  • But two objects/arrays/functions are only equal if they’re the same reference in memory.

So:

{} === {} // false
[] === [] // false
Enter fullscreen mode Exit fullscreen mode

That means if you do this in a parent:

<Child config={{ darkMode: true }} />
Enter fullscreen mode Exit fullscreen mode

React sees a brand new object every render — so even if the contents are the same, React.memo thinks props changed.


How useMemo Fixes This

You can memoize the object so the reference stays the same unless its contents change:

const config = useMemo(() => ({ darkMode: true }), []);
<Child config={config} />
Enter fullscreen mode Exit fullscreen mode

Now, React.memo in Child works as expected — it skips re-rendering when nothing inside config has changed.


Example: Table with Memoized Config

const Table = React.memo(function Table({ config }) {
  console.log("Table rendered");
  return <div>{config.title}</div>;
});

function App() {
  const title = "My Table";

  // Without useMemo: new object each render → Table always re-renders
  const config = useMemo(() => ({ title, sortable: true }), [title]);

  return <Table config={config} />;
}
Enter fullscreen mode Exit fullscreen mode
  • With useMemo → Table only re-renders when title changes.
  • Without → Table re-renders on every parent render.

Where useCallback Comes In

  • useMemo → memoizes values (objects, arrays, computed results).
  • useCallback → memoizes functions.

They solve the same problem (stable references), but for different kinds of things.

Example:

const onClick = useCallback(() => console.log("clicked"), []);
<Button onClick={onClick} />
Enter fullscreen mode Exit fullscreen mode

Now Button won’t re-render just because it got a different onClick function each time.


TL;DR

  • Objects/arrays/functions are compared by reference, not value.
  • Without memoization, new references are created on every render.
  • useMemo (for values) and useCallback (for functions) let you keep stable references.
  • This is one of the most common practical uses of useMemo.

Section 6 — useMemo vs useCallback vs React.memo

These three tools get confused all the time — partly because their names sound similar, and partly because they all relate to “memoization” (fancy word for “remembering a result so you don’t redo work”).

Let’s clear it up once and for all.


1. useMemo — Memoize values

  • You give it a function that returns something (array, object, computed value).
  • It re-runs only when dependencies change.
  • Great for:

    • Expensive derived data
    • Stable object/array props for memoized children

Example:

const tableConfig = useMemo(() => ({ sortable: true }), []);
Enter fullscreen mode Exit fullscreen mode

2. useCallback — Memoize functions

  • You give it a function, and it gives you back the same function reference until dependencies change.
  • Great for:

    • Passing stable callback props to children
    • Avoiding re-renders when the child uses React.memo

Example:

const handleClick = useCallback(() => console.log("clicked"), []);
Enter fullscreen mode Exit fullscreen mode

3. React.memo — Memoize component renders

  • You wrap a component in it.
  • React skips re-rendering that component if props are shallowly equal (compared by value for primitives, by reference for objects/functions).
  • Great for:

    • Optimizing “pure” components that don’t need to update unless props change

Example:

const Button = React.memo(function Button({ onClick }) {
  console.log("Rendered");
  return <button onClick={onClick}>Click</button>;
});
Enter fullscreen mode Exit fullscreen mode

How They Work Together

These tools are often used in combos:

const Button = React.memo(function Button({ config, onClick }) {
  console.log("Rendered");
  return <button onClick={onClick}>{config.label}</button>;
});

function App() {
  const config = useMemo(() => ({ label: "Save" }), []);
  const onClick = useCallback(() => console.log("Saved!"), []);

  return <Button config={config} onClick={onClick} />;
}
Enter fullscreen mode Exit fullscreen mode

Here’s what’s happening:

  • config object stays stable thanks to useMemo.
  • onClick function stays stable thanks to useCallback.
  • Button only re-renders if either prop actually changes, thanks to React.memo.

The Quick Decision Table

You want to… Use…
Memoize a value (array, object) useMemo
Memoize a function useCallback
Skip re-render if props didn’t change React.memo

Pro Tip: Don’t use all three everywhere “just in case.”
Add them when:

  • The component is slow to render, or
  • The value/function is causing avoidable re-renders.

Section 7 — Practical Patterns You’ll Actually Use

useMemo sounds fancy in theory, but let’s be honest — until you see it solve real problems, it can feel like abstract “React trivia.”
So here are the common situations where useMemo isn’t just nice to have… it’s a game-changer.


1. Expensive Derived Data

The scenario:
You’ve got a big dataset — maybe thousands of products, transactions, or users — and you need to sort, filter, or transform it before showing it.

The problem:
Without useMemo, you’ll re-run that heavy transformation on every render, even when nothing related to it changed. That’s wasted work.

Example:

function ProductTable({ products, filter }) {
  const visibleProducts = useMemo(() => {
    console.log("Filtering...");
    return products
      .filter(p => p.category === filter)
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [products, filter]);

  return <List items={visibleProducts} />;
}
Enter fullscreen mode Exit fullscreen mode
  • Only recalculates when products or filter change.
  • If a parent re-renders for unrelated reasons, the cached value is reused.

When to use:
Filtering large lists, doing heavy math, generating big derived objects.


2. Stable Config Objects for Memoized Children

The scenario:
You pass a config object to a child component that’s wrapped in React.memo.

The problem:
Without useMemo, you create a new object each render → child thinks props changed → re-renders anyway.

Example:

const Chart = React.memo(function Chart({ options }) {
  console.log("Chart rendered");
  return <div>Chart goes here</div>;
});

function Dashboard({ data }) {
  const chartOptions = useMemo(() => ({
    type: "line",
    data,
    responsive: true
  }), [data]);

  return <Chart options={chartOptions} />;
}
Enter fullscreen mode Exit fullscreen mode
  • Chart only re-renders if data changes, not just because the parent re-rendered.
  • Saves expensive re-renders, especially for charts/maps/editors.

3. Selector-Style Memoization

The scenario:
You have a huge state object but only care about a small slice of it for this component.

The problem:
If you recalculate that slice on every render, it’s wasteful — especially if it involves non-trivial transformations.

Example:

function Profile({ user }) {
  const fullName = useMemo(() => `${user.firstName} ${user.lastName}`, [user]);
  return <h2>{fullName}</h2>;
}
Enter fullscreen mode Exit fullscreen mode

Yes, this example is cheap, but imagine generating a complex “profile summary” object from user data — that’s where useMemo helps.


4. Memoizing Computed ClassNames or Styles

The scenario:
You’re dynamically generating complex styles or class names based on multiple props.

The problem:
If building that style object is expensive (merging theme objects, computing colors), you don’t want to redo it every render.

Example:

function Card({ isActive, theme }) {
  const styles = useMemo(() => {
    return {
      backgroundColor: isActive ? theme.activeBg : theme.inactiveBg,
      padding: "1rem",
      borderRadius: "8px"
    };
  }, [isActive, theme]);

  return <div style={styles}>Hello</div>;
}
Enter fullscreen mode Exit fullscreen mode

Stable style object → child components won’t re-render unnecessarily.


5. Parsing or Compiling Artifacts

The scenario:
You’re dealing with data that needs to be parsed, compiled, or transformed in a costly way — e.g., parsing large JSON, compiling markdown to HTML, precomputing regex patterns.

Example:

function MarkdownViewer({ content }) {
  const compiled = useMemo(() => markdownToHtml(content), [content]);
  return <div dangerouslySetInnerHTML={{ __html: compiled }} />;
}
Enter fullscreen mode Exit fullscreen mode
  • Only re-parses the markdown when content changes.
  • Saves you from re-running a potentially slow parser every time.

Pattern Takeaways

  • If it’s cheap — skip useMemo.
  • If it’s expensive and you’re re-rendering often — consider it.
  • Memoizing values passed to memoized children is one of the most common real-world uses.

Section 8 — Measuring First (Don’t Guess)

One of the easiest ways to waste time as a developer is to “optimize” something that didn’t need optimizing in the first place.
And yes — useMemo is often guilty of being sprinkled everywhere just because “it sounds fast.”

So let’s make a rule:
Measure before you memoize.


Step 1 — Spotting Symptoms

You might suspect a performance issue if:

  • Typing in an input feels laggy.
  • Clicking a button causes a noticeable delay.
  • Scrolling stutters.
  • Animations aren’t smooth.

But suspicion alone isn’t proof — lots of things can cause lag.
We need data.


Step 2 — Use the React DevTools Profiler

React DevTools has a Profiler tab that lets you record a session and see exactly what’s re-rendering and how long it took.

Basic flow:

  1. Open DevTools → Profiler tab.
  2. Click the “Record” button.
  3. Perform the slow action in your app.
  4. Stop recording.
  5. Look at the flame chart or ranked list.

Step 3 — What to Look For

If you see:

  • Your component re-rendering very often for no good reason
  • Expensive computations running on each render
  • Child components re-rendering just because a prop reference changed

…then you might have a good case for useMemo.

If instead you see:

  • Your component barely takes time to render
  • Most time is spent in the browser painting or layout
  • The lag is from network requests or large DOM updates

…then useMemo won’t help.


Step 4 — Test the Effect of useMemo

If you add useMemo and re-record:

  • Rendering time for that component should drop.
  • Child re-renders should disappear (if it was a referential equality issue).
  • If nothing changes — remove it. No harm in trying, but don’t keep unnecessary code.

Pro Tip:
If your “expensive computation” runs in under ~1ms, useMemo probably won’t be worth the extra mental overhead — React can do the work faster than it can check dependencies and return a cached value.


Section 9 — Pitfalls & Mistakes

useMemo can save you from wasted work — but if you misuse it, you can end up with bugs, stale data, or even slower components.
Let’s look at the most common “gotchas” so you can avoid them.


1. Missing Dependencies (Stale Values)

If your memoized calculation depends on something but you leave it out of the dependency array, React won’t re-run the function when that thing changes — leading to stale results.

const result = useMemo(() => doSomething(data, filter), [data]);
// ❌ forgot `filter` → value never updates when filter changes
Enter fullscreen mode Exit fullscreen mode

Fix: Always include everything your calculation depends on.

const result = useMemo(() => doSomething(data, filter), [data, filter]);
Enter fullscreen mode Exit fullscreen mode

Let ESLint’s react-hooks/exhaustive-deps rule help you here — it’s basically a safety net.


2. Over-Memoizing

Wrapping every calculation in useMemo “just in case” can make your code:

  • Harder to read
  • Slower (because useMemo itself has overhead)
  • More complex to maintain

Rule of thumb: If it’s cheap to compute or the component barely re-renders, skip it.


3. Using useMemo for Side Effects

The function you give useMemo must be pure — no data fetching, no state updates, no DOM manipulation.

Bad:

useMemo(() => {
  fetchData(); // ❌ side effect
  return result;
}, [query]);
Enter fullscreen mode Exit fullscreen mode

If you need to do something (side effect), use useEffect instead.


4. Confusing useMemo and useCallback

  • useMemo memoizes values.
  • useCallback memoizes functions.

If you’re passing a function to a memoized child, use useCallback, not useMemo.


5. Ignoring Referential Equality Problems

Sometimes useMemo is used only to avoid recalculations, but its other superpower is keeping the same reference for objects/arrays.
If you skip this when passing props to React.memo children, you’ll trigger re-renders unnecessarily.


6. Expecting useMemo to Prevent All Renders

useMemo doesn’t stop your component from rendering — it only skips the calculation.
If the parent re-renders, the child still runs — unless you also memoize the child (React.memo).


Pro Tip:
Treat useMemo like a scalpel, not a sledgehammer.
It’s precise, effective, and designed for specific jobs — not for wrapping every single line of code “just because.”


Section 10 — Wrap-up & Mental Checklist

We’ve gone from “useMemo is magic” to “I know exactly when and why to use this thing.”
Now let’s make sure you can spot the right moments in your own code.


The 5-Second useMemo Checklist

Before adding useMemo, ask yourself:

  1. Is the calculation actually expensive?
    If it’s just x * 2 or filtering 5 items — skip it.

  2. Does this component re-render often for unrelated reasons?
    If it only re-renders on prop change, caching may not help.

  3. Will memoizing avoid wasted work?
    Is the same calculation being repeated with the same inputs?

  4. Am I passing objects/arrays/functions to a memoized child?
    If yes, useMemo (or useCallback) can help keep references stable.

  5. Are my dependencies accurate?
    No cheating — include everything the calculation depends on.

If you can answer yes to #1 and either #2 or #4, you’re in “good useMemo territory.”


Key Takeaways

  • useMemo caches the result of a calculation based on dependencies.
  • Best for expensive computations and stable prop references.
  • Not a magic render-optimizer — it only saves you from redoing work.
  • Measure first. Optimize second.
  • Keep functions pure and dependencies complete.

Final Word

Think of useMemo as a polite butler in your component:
It remembers the last task it did, and if you hand it the same instructions again, it just says:

“Here, I already did this for you.”

Use it wisely, and your UI will feel smooth without drowning in unnecessary recalculations.


👉 Coming up next: * The Definitive React 19 useCallback Guide — Patterns, Pitfalls, and Performance Wins*


Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome

Top comments (0)