DEV Community

Cover image for ๐Ÿ”ฅ Your React App is Secretly Slowing Down โ€“ Hereโ€™s How to Fix It With Just One Hook!
Yevhen Kozachenko ๐Ÿ‡บ๐Ÿ‡ฆ
Yevhen Kozachenko ๐Ÿ‡บ๐Ÿ‡ฆ

Posted on • Originally published at ekwoster.dev

๐Ÿ”ฅ 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>;
});
Enter fullscreen mode Exit fullscreen mode

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!");
}, []);
Enter fullscreen mode Exit fullscreen mode

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>;
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)