Written by: ekwoster.dev on Wed Sep 03

🔥 Your React App is Secretly Slowing Down – Here’s How to Fix It With Just One Hook!

🔥 Your React App is Secretly Slowing Down – Here’s How to Fix It With Just One Hook!

Cover image for 🔥 Your React App is Secretly Slowing Down – Here’s How to Fix It With Just One Hook!

🔥 Your React App is Secretly Slowing Down – Here’s How to Fix It With Just One Hook!

React is loved for its simplicity, declarative style, and component-based architecture. But beneath the surface of even the cleanest codebases lies a haunting truth – re-renders are silently sabotaging your performance.

This post is not another basic "Use React.memo!" kind of article. Instead, we're diving deep into a lesser-used yet incredibly powerful hook that can magically save your app from performance death: useCallback — and more importantly, how and when to use it correctly.

In this post, you'll learn:

  • 👉 Why your app is slowing down despite using React.memo
  • 👉 What really triggers re-renders
  • 👉 What useCallback solves (and what it doesn't)
  • 👉 A step-by-step code example translating laggy UI into buttery smooth UX
  • 👉 A custom hook trick to analyze what components are re-rendering — like a profiler!

😱 The Hidden Performance Problem

Let’s say you have a parent component passing a function to a child.

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("Clicked!");
  };

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <MemoizedChild onClick={handleClick} />
    </div>
  );
}

const MemoizedChild = React.memo(({ onClick }) => {
  console.log("Child rendered");
  return <button onClick={onClick}>Click Me</button>;
});

You'd expect MemoizedChild to not re-render when count changes, right? WRONG.

😬 Why?

Because handleClick is re-created on every render. For React.memo, new function === new prop, so the memoized component re-renders.


✅ Enter useCallback

const handleClick = useCallback(() => {
  console.log("Clicked!");
}, []);

Now, handleClick has a stable reference until the dependencies change (empty in this case), so MemoizedChild doesn’t re-render unnecessarily.

Let’s verify it step by step.


🕵️‍♂️ Create a Render Visualizer to Spot Unwanted Renders

A neat trick to help debug performance:

function useRenderTracker(name) {
  const renders = useRef(0);
  useEffect(() => {
    renders.current++;
    console.log(`${name} rendered ${renders.current} times`);
  });
}

function Parent() {
  useRenderTracker("Parent");
  // ...
}

const MemoizedChild = React.memo(function Child({ onClick }) {
  useRenderTracker("Child");
  return <button onClick={onClick}>Click</button>;
});

Nothing like real-time logs to show the hidden performance creepers. Run both versions (with and without useCallback) and observe the difference in renders.


❌ But Don’t Overuse useCallback

Now, before you go and wrap every function call inside useCallback, hold up!

⚠️ Common pitfalls:

  • It adds complexity
  • Recreating the callback can often be cheaper than memoizing
  • If the function isn’t passed to a memoized child component or used in a dependency array, it’s likely unnecessary

🔑 Rule of thumb:

Use useCallback only when passing callbacks to memoized components or using them inside useEffect/useMemo dependency arrays.


⚙️ A Real-World Optimization: Dynamic Search UI

React apps often suffer from performance hits when passing state-updating functions to children “search boxes,” especially during typing.

Here’s an optimized example:

function SearchForm({ onChange }) {
  return <input onChange={e => onChange(e.target.value)} />;
}

const MemoizedSearchForm = React.memo(SearchForm);

function App() {
  const [query, setQuery] = useState("");

  const handleSearch = useCallback((value) => {
    setQuery(value);
  }, []);

  return (
    <div>
      <MemoizedSearchForm onChange={handleSearch} />
      <ResultsList query={query} />
    </div>
  );
}

Without useCallback, unnecessary re-renders might make your search feel slower.


💡 Bonus: Babel Plugin to Track Anonymous Functions

To really hammer down these problems, there are tools like eslint-plugin-react-perf or even custom Babel transforms that warn you when you're passing anonymous functions as props.


🧠 Final Thoughts

Most performance pain in medium-large React apps comes from unintended re-renders, often due to unstable function references. This is one of the less intuitive performance bugs because the UI looks fine — until it doesn’t.

🚀 Learn to:

  • Use useCallback sparingly but purposefully
  • Understand when child components really need to re-render
  • Leverage React.memo AND stable props

This small shift in mindset can drastically improve the feel of your app — and your users will feel it too.

Happy coding!


🔗 Further Reading


👉 If you need professional help optimizing your React frontend for performance, we offer frontend development services.