Introduction
React is designed to be fast, but as applications grow in size and complexity, unnecessary re-renders and expensive calculations can affect performance. Functional components, while elegant, can sometimes rerender more often than necessary.
React provides two hooks—useMemo and useCallback—to help optimize performance by memoizing values and functions. These hooks prevent components from recomputing values or recreating functions on every render, reducing the rendering workload and improving efficiency.
In this article, we will explore:
- How
useMemohelps memoize computed values. - How
useCallbackhelps memoize functions. - How to avoid unnecessary re-renders using these hooks.
- Practical examples of expensive calculations and memoization.
What is useMemo?
useMemo is a hook that memoizes the result of a computation. It only recomputes the value when its dependencies change, preventing expensive calculations from running on every render.
Syntax
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- First argument: a function that returns a value.
- Second argument: dependency array; the value recomputes only when dependencies change.
Basic Example of useMemo
import React, { useState, useMemo } from "react";
function ExpensiveComponent({ number }) {
const expensiveCalculation = (num) => {
console.log("Calculating...");
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num;
}
return result;
};
const computedValue = useMemo(() => expensiveCalculation(number), [number]);
return (
<div>
<p>Computed Value: {computedValue}</p>
</div>
);
}
function App() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(5);
return (
<div>
<ExpensiveComponent number={number} />
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<button onClick={() => setNumber(number + 1)}>Increment Number</button>
<p>Count: {count}</p>
</div>
);
}
Explanation
- The expensive calculation runs only when
numberchanges. - Updating
countdoes not trigger recalculation, improving performance.
What is useCallback?
useCallback is a hook that memoizes a function, returning the same function instance unless its dependencies change. This prevents unnecessary function recreation and helps avoid re-rendering child components that rely on function props.
Syntax
const memoizedCallback = useCallback(() => {
// function logic
}, [dependencies]);
- First argument: the function to memoize.
- Second argument: dependency array; the function is recreated only when dependencies change.
Example of useCallback
import React, { useState, useCallback } from "react";
function Button({ onClick, label }) {
console.log("Button rendered");
return <button onClick={onClick}>{label}</button>;
}
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Button onClick={handleClick} label="Increment" />
</div>
);
}
Explanation
- Without
useCallback,handleClickwould be recreated on every render, causingButtonto re-render unnecessarily. useCallbackensuresButtonreceives the same function instance unless dependencies change.
Why useMemo and useCallback Improve Performance
1. Memoizing Expensive Computations
useMemo avoids recomputing values that take significant time or resources.
2. Preventing Unnecessary Re-Renders
useCallback ensures child components depending on function props don’t re-render unless necessary.
3. Reducing Render Workload
By memoizing values and functions, React avoids executing computations or recreating functions on every render.
Avoiding Unnecessary Re-Renders
Example: Without Memoization
function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
}
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = () => setCount(count + 1);
return (
<div>
<Child onClick={handleClick} />
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>Count: {count}</p>
</div>
);
}
- Every time
textchanges,Parentre-renders. Childre-renders becausehandleClickis a new function each time.
Example: Using useCallback to Avoid Re-Render
import React, { useState, useCallback } from "react";
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = useCallback(() => setCount((prev) => prev + 1), []);
return (
<div>
<Child onClick={handleClick} />
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>Count: {count}</p>
</div>
);
}
Explanation
Childis wrapped withReact.memo.handleClickis memoized usinguseCallback.- Now
Childdoes not re-render whentextchanges.
Expensive Calculation Memoization
Scenario: Fibonacci Sequence
Calculating Fibonacci numbers recursively is expensive. Memoization avoids recalculating the sequence on each render.
import React, { useState, useMemo } from "react";
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function FibonacciComponent() {
const [num, setNum] = useState(35);
const [count, setCount] = useState(0);
const fibValue = useMemo(() => fibonacci(num), [num]);
return (
<div>
<p>Fibonacci({num}) = {fibValue}</p>
<button onClick={() => setCount(count + 1)}>Increment Count ({count})</button>
<input type="number" value={num} onChange={(e) => setNum(Number(e.target.value))} />
</div>
);
}
Explanation
- Fibonacci calculation is only recomputed when
numchanges. - Updating
countdoes not trigger recomputation, improving performance.
Combining useMemo and useCallback
import React, { useState, useMemo, useCallback } from "react";
function ExpensiveList({ items, onItemClick }) {
console.log("ExpensiveList rendered");
return (
<ul>
{items.map((item) => (
<li key={item} onClick={() => onItemClick(item)}>{item}</li>
))}
</ul>
);
}
const MemoizedList = React.memo(ExpensiveList);
function App() {
const [count, setCount] = useState(0);
const [input, setInput] = useState("");
const items = useMemo(() => {
console.log("Generating items");
return Array.from({ length: 1000 }, (_, i) => Item ${i});
}, []);
const handleClick = useCallback((item) => {
console.log("Clicked:", item);
}, []);
return (
<div>
<MemoizedList items={items} onItemClick={handleClick} />
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type here" />
<button onClick={() => setCount(count + 1)}>Increment Count ({count})</button>
</div>
);
}
Explanation
itemsarray is memoized usinguseMemo.handleClickfunction is memoized usinguseCallback.MemoizedListdoes not re-render unnecessarily.
Best Practices for useMemo and useCallback
- Use only for expensive computations or frequent re-renders
- Don’t overuse; memoization has overhead.
- Keep dependency arrays accurate
- Include all values used inside the memoized function.
- Combine with React.memo
- Helps prevent child component re-renders.
- Avoid inline functions for props
- Use
useCallbackto prevent function recreation.
- Use
- Profile before optimizing
- Use React DevTools profiler to identify bottlenecks.
Common Pitfalls
- Over-memoization
- Memoizing cheap calculations or functions can reduce performance.
- Incorrect dependencies
- Omitting dependencies can lead to stale values.
- Using memoization unnecessarily
- For small apps or lightweight calculations, memoization is often unnecessary.
Leave a Reply