Skip to content

React Performance: useMemo & useCallback (Casual Guide)

Posted on:January 9, 2026 at 10:00 AM

Hey there, Fellow Coder! šŸ‘‹

Ever built a React app that felt snappy at first, but as you added features, it started to feel… heavy? Like, you type into an input field and there’s a tiny, annoying delay? Or animations just aren’t smooth anymore?

This is where performance optimization comes in. And in React, two of the most popular tools in our utility belt are useMemo and useCallback.

But here’s the catch: a lot of devs use them everywhere, thinking it makes things faster. Spoiler alert: sometimes it makes things slower! 😱

So, grab your coffee ā˜•, and let’s break down exactly when and how to use these hooks properly. No rocket science, just plain talk.

The Problem: Re-renders are ā€œCheapā€, but Calculations are Expensive

First, let’s understand how React works. When state changes, React re-renders the component. This means it runs your component function again from top to bottom.

Usually, this is super fast. React is smart. But, imagine you have a function inside your component that does some heavy math:

const heavyCalculation = (num) => {
  console.log("Calculating...");
  // Simulate a slow process (e.g., filtering a huge list)
  for (let i = 0; i < 1000000000; i++) {}
  return num * 2;
};

If your component re-renders because you typed in a different input field, this heavyCalculation runs again. Even if num didn’t change! That’s wasted effort, and that’s what freezes your UI.


1. useMemo: The Result Cacher

useMemo is like a smart notepad. It remembers the result of a calculation.

Analogy: Imagine you ask me, ā€œWhat’s 23,452 x 9,123?ā€ I pull out a calculator, spend 10 seconds, and tell you the answer. If you ask me the exact same question 5 seconds later, I shouldn’t grab the calculator again. I should just remember the answer I just gave you.

That’s useMemo.

How to use it:

import { useState, useMemo } from 'react';

const ExpensiveComponent = () => {
  const [count, setCount] = useState(0);
  const [dark, setDark] = useState(false);

  // Without useMemo, this runs on EVERY render (even when toggling dark mode!)
  // const doubleNumber = heavyCalculation(count);

  // WITH useMemo:
  const doubleNumber = useMemo(() => {
    return heavyCalculation(count);
  }, [count]); // Only run if 'count' changes

  const themeStyles = {
    backgroundColor: dark ? 'black' : 'white',
    color: dark ? 'white' : 'black'
  };

  return (
    <div style={themeStyles}>
      <input type="number" value={count} onChange={e => setCount(parseInt(e.target.value))} />
      <button onClick={() => setDark(prevDark => !prevDark)}>Change Theme</button>
      <div style={themeStyles}>{doubleNumber}</div>
    </div>
  );
}

Now, if you click ā€œChange Themeā€, the component re-renders to update the styles, but it skips the heavy calculation because count hasn’t changed. Smooth! ⚔


2. useCallback: The Function Freezer

This one is trickier. useMemo caches a value (like a number or object). useCallback caches a function definition.

ā€Wait, why would I want to cache a function? Creating functions is cheap!ā€

True. But in JavaScript, Referential Equality is a thing. Every time a component re-renders, any function defined inside it is technically a new function. It’s a brand new reference in memory.

The Problem: If you pass this function as a prop to a child component that is wrapped in React.memo, the child will say: ā€œHey, new prop! New function! I must re-render!ā€

How to use it:

import { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';

const Parent = () => {
  const [count, setCount] = useState(0);
  const [toggle, setToggle] = useState(false);

  // This function is re-created on every render
  // const increment = () => setCount(c => c + 1);

  // This function stays the same reference unless 'setCount' changes (which it doesn't)
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, [setCount]);

  return (
    <div>
      <ChildComponent onIncrement={increment} />
      <button onClick={() => setToggle(!toggle)}>Toggle</button>
    </div>
  );
}

In this case, clicking ā€œToggleā€ re-renders the Parent. But because increment is cached via useCallback, the ChildComponent (if optimized) sees the same prop and doesn’t uselessly re-render.


When NOT to use them āš ļø

Don’t go wrapping everything in useMemo and useCallback. Why?

  1. Code complexity: It makes code harder to read.
  2. Memory overhead: You’re trading CPU time for memory usage.
  3. Performance cost: Running the logic to check if dependencies changed takes time too! For simple things, useMemo might actually be slower than just re-calculating.

Rule of Thumb: Only use them when:

Closing

So there you have it!

Performance optimization is an art. Don’t optimize prematurely. Build it first, measure it, and then apply these tools if things feel sluggish.

Happy Coding! šŸš€