React is fast. By default, it is designed so that you don’t have to worry about performance in 90% of cases.
However, when applications grow, we start to notice some “lag” when typing in an input or opening a menu. Generally, this is due to one of the most common problems in React: unnecessary renders.
An unnecessary render is when a component is drawn (itself and its children), without it really being necessary because nothing had changed.
One of the mechanisms to improve this is Memoization, a technique that allows “remembering” previous results so they don’t have to be recalculated.
The Problem of Cascading Renders
To understand the solution, you first need to understand the problem. In React, when a parent component renders (because its state changed), all its children render recursively as well, whether their props changed or not.
Imagine this:
You have a Parent component with a description state (for example).
You have a Child component that is a static list of 5,000 elements.
Every time you increment the parent’s description, React repaints the list of 5,000 elements, even though it hasn’t changed at all 😱.
This is crazy inefficient. This is where our three optimization tools come in.
Protecting Components with React.memo
React.memo is a Higher-Order Component (HOC) that wraps your component. Its function is to check the props:
If the props it receives are identical to the previous time, it does not re-render, it reuses the previous result.
The syntax is as follows:
import { memo, useState } from 'react';
// Child Component (Heavy)
// By wrapping it in memo, we protect it.
const HijoPesado = memo(function Hijo({ texto }) {
console.log("Renderizando Hijo..."); // Will only appear if 'texto' changes
return <p>{texto}</p>;
});
export default function Padre() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{/* Even though Parent changes, 'texto' is still "Hola",
so HijoPesado will NOT render again. */}
<HijoPesado texto="Hola" />
</div>
);
}
React.memo does a shallow comparison (Object.is). If you pass objects or functions as props, you will need the tools we will see next, or the memoization will fail.
Stabilizing Functions with useCallback
This is the most difficult Hook to understand, because it has to do with Referential Identity in JavaScript.
In JS, function() {} !== function() {}.
Every time a component renders, all the functions inside it are created again. They are new functions at new memory addresses.
Let’s see the problem. Let’s go back to React.memo. We said it only prevents rendering if the props don’t change.
function Padre() {
const [count, setCount] = useState(0);
// This function IS CREATED ANEW on every click
const handleClick = () => console.log('Click');
return (
// ¡React.memo will NOT work!
<HijoMemoizado onClick={handleClick} />
);
}
For the Child, the prop ‘onClick’ has changed (it’s a new function), so it renders anyway.
To avoid this, useCallback freezes the function. It returns exactly the same function instance between renders, unless its dependencies change.
import { useCallback } from 'react';
function Padre() {
const [count, setCount] = useState(0);
// ✅ Now 'handleClick' is stable. It's always the same reference in memory.
const handleClick = useCallback(() => {
console.log('Click');
}, []); // Empty dependencies = never changes
return (
// 😄 Now yes: HijoMemoizado receives the same prop and does NOT render.
<HijoMemoizado onClick={handleClick} />
);
}
You only need useCallback if you are going to pass that function as a prop to a component wrapped in React.memo or if the function is a dependency of a useEffect.
In all other cases, it adds nothing.
The Cost of Memoization (a.k.a. don’t use it for everything)
If these tools are so good, why not wrap everything in memo and useCallback? Because memoization is not free.
- Consumes Memory: React has to store the previous inputs and the previous result in memory.
- CPU Cost: On every render, React has to compare the new props with the old ones.
If the component is simple (e.g., a <Button>Text</Button>), it costs more work to compare the props than to render the button again.
- Visually large components (Lists, Tables, Charts)
- Components that render very often (on scroll, when typing in an input)
- When you notice real slowness
- Simple Buttons, Icons, Texts
- Components that always change (if the props always change, memo works for nothing)
- “Just in case”, or because you’re bored… (premature optimization)
