Did You Know useCallback Can Actually Do This?
Muhammad Athar

If you've been following along with Alex Chen's How to Build a Custom Hook That Actually Earns Its Abstraction or our own useEffect, What Is It Really? you've seen useCallback show up without much ceremony.
In the custom hooks article, there's this line inside useLocalStorage:
And if you've been reading carefully you might have thought — I understand what's happening here, but I don't quite understand why useCallback is wrapping that function. Why not just define it normally?
That's exactly what this article answers.
Functions are recreated on every render
Here's the thing about JavaScript functions inside React components that trips up almost every developer at some point.
Every time a React component re-renders, every function defined inside it is created fresh. New reference. New memory address. Even if the code is identical.
Most of the time this doesn't matter. The function does its job and gets garbage collected. But it becomes a problem in two specific situations:
- When the function is a dependency in a
useEffectarray - When the function is passed as a prop to a component wrapped in
React.memo
In both cases, a new function reference on every render means the effect re-runs or the memoized component re-renders — even when nothing meaningful changed.
useCallback fixes this by giving you back the same function reference between renders, unless its dependencies change.
The basic shape
Same structure as useEffect — a function and a dependency array. The difference: useEffect runs a function after render. useCallback returns a function that stays stable between renders.
React will only create a new function when something in the dependency array changes. Otherwise it hands back the same reference it gave you last time.
The useEffect dependency problem
Here's the scenario where useCallback earns its place most clearly.
You have a component that fetches data. The fetch logic is in a function. You want to call that function inside a useEffect. So you add the function to the dependency array — because the linter tells you to, and the linter is right.
This creates an infinite loop. The component renders → fetchUser is created → useEffect runs → setUser triggers a re-render → fetchUser is created again → useEffect runs again → forever.
useCallback breaks the loop:
Now fetchUser only gets a new reference when userId changes. The effect only re-runs when userId changes. The loop is broken.
Building something real: a dropdown component
Here's a complete accessible dropdown menu that demonstrates useCallback in a context you'll actually encounter — event handlers that need to be stable because they're attached to the document.
You can use it like this:
Two places useCallback is doing real work here:
handleClickOutside — this function gets added to document as an event listener inside a useEffect. Without useCallback, every render would produce a new function reference, and the effect would remove and re-add the listener constantly. With useCallback and an empty dependency array, the same function reference is reused for the lifetime of the component.
handleSelect — this function depends on onChange, which comes from the parent. useCallback ensures that if the parent re-renders but onChange hasn't actually changed, handleSelect stays stable too.
When useCallback is not the answer
useCallback is not a general performance tool. Wrapping every function in your component with it actually makes things slightly slower — React has to store the previous function, compare dependencies, and decide whether to return the cached version. That overhead exists even when the memoization never helps.
The two cases where it genuinely earns its place:
- A function is in a
useEffectdependency array — useuseCallbackto prevent the effect re-running on every render - A function is passed as a prop to a
React.memocomponent — useuseCallbackto prevent the memoized component re-rendering unnecessarily
Everything else — event handlers on regular HTML elements, functions used only inside the component, callbacks that aren't dependencies — can be defined without useCallback.
The pattern to remember
Same function. Different identity. useCallback is about referential stability, not about making the function run faster.
Where this leads
useMemo — useCallback memoizes a function. useMemo memoizes a value. Same idea, different shape. Coming up next in Make Use of useMemo Like You Mean It.
React.memo — the component-level counterpart to useCallback. Wraps a component so it only re-renders when its props change. useCallback and React.memo are most useful together — stable function references passed to memoized components. Alex covers this in Why React.memo Doesn't Always Help.
useRef — another tool for keeping a stable reference across renders, but for values rather than functions. We'll get to it in the next "From the Builder" article.
This article was prompted by the React Foundations series — particularly How to Build a Custom Hook That Actually Earns Its Abstraction and useEffect, What Is It Really?. If a function in a dependency array has ever caused you an infinite loop, now you know why — and how to fix it.
Muhammad Athar is the founder and engineer behind DesignDev.io. He writes the "From the Builder" series — concept explainers triggered by the site's own articles, always grounded in something you can actually build.

