Optimising Performance in React with useMemo and useCallback

Performance issues in React often come from unnecessary re‑renders and expensive computations. Each time a component re‑renders, React recreates functions and recalculates values, even if they have not changed. This is usually fine for small applications, but as complexity grows, unnecessary re‑renders can slow things down.
React provides useMemo and useCallback to reuse calculation results and function references when their dependencies remain unchanged. They are performance optimisations, so the component must still work if React discards a cache. Here I'll show what they can save and where the extra code is worth measuring.
Understanding Component Re‑Renders in React
Each time a component's state or props change, React re‑renders it. Normally, this is not a problem, but if the component contains expensive calculations or function creations, performance can suffer.
Consider this example:
const ExampleComponent = ({ count }: { count: number }) => {
console.log("Component re-rendered");
const computeExpensiveValue = () => {
console.log("Expensive computation running");
return count * 100;
};
return <p>Result: {computeExpensiveValue()}</p>;
};Every time ExampleComponent re‑renders, computeExpensiveValue runs again, even if count has not changed. This is where useMemo can help.
Improving Performance with useMemo
What Is useMemo?
useMemo caches a calculated value. React compares each dependency using Object.is and can reuse the value when none has changed. The calculation must remain pure and correct if React runs it again.
Example: Preventing Unnecessary Computations
We can optimise the previous example by using useMemo, like this:
const ExampleComponent = ({ count }: { count: number }) => {
console.log("Component re-rendered");
const computedValue = useMemo(() => {
console.log("Expensive computation running");
return count * 100;
}, [count]);
return <p>Result: {computedValue}</p>;
};React can reuse computedValue when count is unchanged. The multiplication is deliberately small so we can follow the example; it would rarely justify memoisation by itself.
When to Use useMemo
useMemo is useful when:
- A calculation is expensive and should not run on every render.
- The result only needs to change when specific values update.
- A large dataset needs filtering, sorting, or processing.
However, overusing useMemo can make code harder to read, so it should only be used when performance is a concern.
Reusing Function Identity with useCallback
What Is useCallback?
useCallback can return the previous function reference whilst its dependencies remain unchanged. This can help a memoised child skip work when that function is a prop and every other prop is also unchanged.
Example: Comparing Callback Identity
Consider this simple counter component:
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
console.log("Increment clicked");
setCount((prev) => prev + 1);
};
return <button onClick={increment}>Increment</button>;
};Each render creates a new increment function here. The console message runs when the button is clicked, so it demonstrates invocation rather than function creation. Creating a small callback is normally cheap; the host button alone is not a reason to memoise it.
We can retain the function identity using useCallback, although that alone is not a performance optimisation:
import { useCallback, useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
console.log("Increment clicked");
setCount((prev) => prev + 1);
}, []);
return <button onClick={increment}>Increment</button>;
};The function expression passed to useCallback is still created during rendering. React can return the previously cached reference as increment when the dependencies are unchanged. The click log does not prove that reuse; it reports invocation. Reusing identity matters when another boundary observes it, such as a memoised child or an Effect dependency.
When to Use useCallback
useCallback is useful when:
- Passing a callback to a child wrapped in
React.memo, when the callback's changing identity causes otherwise avoidable work. - Keeping a callback stable when it is a dependency whose identity change would repeat work.
- Profiling shows that the saved work outweighs the cost and complexity of maintaining the cache.
Combining useMemo and useCallback
It's pretty common to see these hooks used together, particularly when working with lists, filtering, and memoised components.
Example: Optimising a List Filter
Imagine filtering a list of names based on user input:
import { useMemo } from 'react';
const names = ["Alice", "Bob", "Charlie", "David"];
const NameList = ({ query }: { query: string }) => {
const filteredNames = useMemo(() => {
console.log("Filtering names");
return names.filter((name) => name.toLowerCase().includes(query.toLowerCase()));
}, [query]);
return (
<ul>
{filteredNames.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
);
};The fixed names array now lives outside the component, so query is the only changing input to the calculation. React can reuse filteredNames when query is unchanged; the component remains correct if it calculates again.
Using NameList from the previous example, we can add a labelled filter input and a reset button in the parent:
import { memo, useCallback, useState } from 'react';
const ResetButton = memo(({ onReset }: { onReset: () => void }) => {
console.log("ResetButton rendered");
return <button onClick={onReset}>Reset</button>;
});
const ParentComponent = () => {
const [query, setQuery] = useState("");
const resetQuery = useCallback(() => {
setQuery("");
}, []);
return (
<>
<label>
Filter names
<input value={query} onChange={(e) => setQuery(e.target.value)} />
</label>
<ResetButton onReset={resetQuery} />
<NameList query={query} />
</>
);
};Here, resetQuery can keep the onReset prop stable whilst its dependencies are unchanged. That gives the memoised ResetButton an opportunity to skip work when the parent renders for a query change. The ResetButton log reports renders; the earlier counter logs report clicks.
When Not to Use useMemo and useCallback
Whilst these hooks can improve performance, they are not always necessary.
Avoid using them when:
- The calculation is not expensive and runs quickly anyway.
- The callback's identity is not observed by a memoised child or a dependency that would otherwise repeat work.
- The component does not re‑render often enough to justify memoisation.
React's default rendering behaviour is usually efficient, so these hooks should only be used when performance becomes an issue.
Measure before and after in a production or profiling build. Use React DevTools Profiler to check render counts and actual duration for the affected subtree; remove the memoisation if the measured saving does not outweigh its complexity.
Wrapping Up
useMemo can reuse a calculated value, and useCallback can reuse a function reference. Neither is a correctness guarantee or a way to prevent function expressions being created. I would keep them where profiling shows a useful saving and the dependencies remain easy to understand.
Key Takeaways
useMemocan reuse a calculation result when its dependencies are unchanged; the calculation must still be safe to run again.useCallbackcan preserve a function reference whilst its dependencies are unchanged, which may help a memoised child or another identity‑sensitive dependency.- Both hooks should only be used when performance issues arise, as unnecessary use can make code harder to maintain.
- React's default rendering behaviour is usually efficient, and these hooks should only be used for optimisations when needed.
When used in the right places, these hooks help keep React applications running smoothly without adding unnecessary complexity.