React.js

Avoiding Common Pitfalls with useCallback and useMemo in React

Modern React development encourages performance optimization, and two hooks that often come up are useCallback and useMemo. While these hooks can be powerful, they are frequently overused, sometimes even harming performance instead of improving it.

This article explains when to use them, when to avoid them, and how to write more efficient React code without unnecessary complexity.

1. Understanding the Purpose of useCallback and useMemo

Before discussing overuse, it is important to understand what these hooks actually do.

useCallback

useCallback is used to memoize a function so that React does not recreate it on every render unless its dependencies change.

import { useCallback } from "react";

function Example( { value }) {
    const handleClick = useCallback(() => {
        console.log(value);
    }, [value]);
    return <button onClick={handleClick}>Click</button>;
}

In this example, the handleClick function is only recreated when value changes. On the surface, this seems like a good optimization.

However, what actually happens behind the scenes is that React stores the previous function reference and compares dependencies on every render. This means there is a cost associated with using useCallback. If the function is cheap to create, this cost may outweigh the benefit.

useMemo

Just like useCallback, useMemo is designed for optimization, but instead of memoizing functions, it memoizes computed values. It allows React to reuse the result of a calculation instead of recomputing it on every render.

import { useMemo } from "react";

function Example( { items }) {
    const filteredItems = useMemo(() => {
        return items.filter(item => item.active);
    }, [items]);
    return <div>{filteredItems.length}</div>;
}

Here, the filtering logic only runs when items changes. While this looks efficient, it is only beneficial if the filtering operation is expensive or the component re-renders frequently. Otherwise, the overhead of tracking dependencies and storing results can make things worse.

2. The Hidden Cost of Memoization

Memoization is often treated as a free optimization, but it introduces its own overhead. React needs to track dependencies, store previous values, and perform comparisons on every render.

const result = useMemo(() => computeExpensiveValue(data), [data]);

Although this avoids recomputing computeExpensiveValue, React still performs dependency checks and memory management. If computeExpensiveValue is actually fast, then the memoization layer adds unnecessary work.

This is why blindly adding useMemo or useCallback everywhere can degrade performance instead of improving it. The key is to understand that memoization should be used selectively, not by default.

3. Overusing useCallback for Simple Functions

One of the most common mistakes is wrapping every function in useCallback, even when there is no real benefit.

function Counter( { count }) {
    const increment = useCallback(() => {
        console.log("Increment clicked");
    }, []);
    return <button onClick={increment}>{count}</button>;
}

In this case, the increment function does not depend on any changing values and is not passed to a memoized child component. React can recreate this function extremely quickly, so memoizing it adds unnecessary complexity.

A simpler and more effective approach is to define the function normally.

function Counter( { count }) {
    const increment = () => {
        console.log("Increment clicked");
    };
    return <button onClick={increment}>{count}</button>;
}

This version is easier to read and avoids the overhead of dependency tracking. In most scenarios, this is sufficient.

4. Using useCallback Without React.memo

Another common misuse of useCallback is using it in isolation without pairing it with React.memo.

function Parent() {
    const handleClick = useCallback(() => {
        console.log("Clicked");
    }, []);
    return <Child onClick={handleClick} />;
}

At first glance, this looks optimized. However, if the Child component is not memoized, it will re-render every time the parent renders regardless of whether handleClick changes.

To make useCallback effective, the child component must be memoized.

const Child = React.memo(({ onClick }) => {
    console.log("Child rendered");
    return <button onClick={onClick}>Click</button>;
});
function Parent() {
    const handleClick = useCallback(() => {
        console.log("Clicked");
    }, []);
    
    return <Child onClick={handleClick} />;
}

Now, React can skip re-rendering Child if its props remain the same. This is one of the few scenarios where useCallback provides real value.

5. Overusing useMemo for Cheap Computations

Developers often use useMemo for very simple calculations that do not need optimization.

function User( { firstName, lastName }) {
    const fullName = useMemo(() => {
        return firstName + " " + lastName;
    }, [firstName, lastName]);
    return <div>{fullName}</div>;
}

This is unnecessary because string concatenation is extremely fast. The memoization overhead is more expensive than the computation itself. A better approach is to compute the value directly.

function User( { firstName, lastName }) {
    const fullName = firstName + " " + lastName;
    return <div>{fullName}</div>;
}

This keeps the code simple and avoids unnecessary optimization logic.

6. Misusing useMemo for Derived State

Another pattern is using useMemo for derived data that does not need memoization.

function List( { items }) {
    const activeItems = useMemo(() => {
        return items.filter(item => item.active);
    }, [items]);

    return (
            <ul> {activeItems.map(item => (
            <li key={item.id}>{item.name}</li>
            ))} 
           </ul>
  );
}

This approach is only beneficial if the items array is very large or the filtering operation is computationally expensive. In many cases, it is perfectly fine to compute this directly during rendering.

function List( { items }) {
    const activeItems = items.filter(item => item.active);
    
    return (
      <ul> 
       {activeItems.map(item => (
               <li key={item.id}>{item.name}</li>
               ))} 
       </ul>
   );
}

This version is simpler and avoids unnecessary memoization unless performance profiling proves otherwise.

7. When You Should Use useCallback

Despite the risks of overuse, useCallback does have valid use cases when applied correctly.

const Item = React.memo(({ onSelect }) => {
    return <button onClick={onSelect}>Select</button>;
});
function List() {
    const handleSelect = useCallback(() => {
        console.log("Item selected");
    }, []);
    
    return <Item onSelect={handleSelect} />;
}

In this scenario, useCallback ensures that the function reference remains stable, allowing the memoized Item component to avoid unnecessary re-renders. The key takeaway is that useCallback is most useful when function identity affects rendering behavior.

8. When You Should Use useMemo

useMemo is most effective when dealing with expensive computations or large datasets.

function Dashboard( { data }) {
    const sortedData = useMemo(() => {
        return [...data].sort((a, b) => a.value - b.value);
    }, [data]);
    
    return <div>{sortedData.length}</div>;
}

Sorting large datasets can be expensive, so memoizing the result can significantly improve performance if the component re-renders frequently. However, this should only be done after identifying a real performance bottleneck.

9. Measure Before You Optimize

Optimization should always be driven by measurement, not assumptions. React provides tools to help identify performance issues.

console.time("filter");
const result = items.filter(item => item.active);
console.timeEnd("filter");

You can also use the React DevTools Profiler to analyze component re-renders and identify bottlenecks. Without measurement, adding memoization is just guesswork and often leads to unnecessary complexity.

Cleaner React Code Without Over-Memoization

One of the most important principles in React development is prioritizing readability and simplicity.

function Dashboard( { users }) {
    const activeUsers = users.filter(user => user.active);

    return (
            <ul>
                {activeUsers.map(user => (
                  <li key={user.id}>{user.name}</li>
              ))} 
            </ul>
   );
}

This example avoids unnecessary hooks and keeps the logic straightforward. In most cases, this approach is not only easier to maintain but also performs well enough.

10. Conclusion

Overusing useCallback and useMemo is a common pitfall in React development. While these hooks are powerful, they are not meant to be used everywhere.

They introduce overhead, increase code complexity, and can even reduce performance when used incorrectly. The best approach is to start with simple, readable code and only introduce memoization when a real performance issue has been identified.

By focusing on clarity first and optimization second, you can build React applications that are both efficient and easy to maintain.

This article explored how to avoid overusing useCallback and useMemo in React.

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button