Skip to content
DevDepth
← Back to all articles

In-Depth Article

How useCallback Works in React: Dependency Checks, Fiber Storage, and Function Reuse

Learn what useCallback really does in React: when it stores [callback, deps], how Object.is dependency checks reuse an older function reference, why it is basically useMemo for functions, and where Hook memory lives on Fiber.

Published: Updated: 7 min readreact-internals

useCallback is often explained as "React caches your function so it does not get recreated." That description points in the right direction, but it is still a little too fuzzy.

The more precise version is this:

JavaScript still creates the function you wrote during render. useCallback only decides whether React should return that new function or reuse the previous one it already stored.

Once that clicks, the rest becomes much easier to understand:

  • useCallback is basically useMemo for function references
  • its dependency check is the same element-by-element comparison used by other Hooks
  • its "memory" comes from the same Hook storage system that powers useState, useMemo, and other Hooks on a Fiber

If you want nearby context first, this article pairs well with why Hooks must keep a stable call order, why callbacks can read old state through stale closures, and how React turns state updates into a real render and commit.

1. Start with the most important correction: useCallback does not stop function creation

Take a common example:

import { useCallback } from "react";

function ProductPage({ productId }) {
  const handleBuy = useCallback(() => {
    console.log("buy", productId);
  }, [productId]);

  return <button onClick={handleBuy}>Buy</button>;
}

On every render, the component function runs again. That means the arrow function () => { console.log("buy", productId); } is also created again by JavaScript.

So what is useCallback actually doing?

It is not preventing function creation at the language level. It is making a later decision:

  • if the dependencies changed, React keeps the new function
  • if the dependencies did not change, React throws away that newly created function and returns the older stored one instead

That is why useCallback is about reference reuse, not "do not create a function at all."

This detail matters because it explains two common surprises:

  • useCallback is not automatically useful on every render path
  • useCallback([]) can still produce stale closures, because the stable function may keep reading data from the render where it was created

2. What happens on the first render: React stores [callback, deps]

During the mount phase, useCallback(callback, deps) is conceptually simple.

React creates a Hook node for the current call site, stores the callback together with its dependency array, and returns the callback you passed in.

You can think of the logic like this:

function mountCallback(callback, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;

  hook.memoizedState = [callback, nextDeps];
  return callback;
}

Three things matter here:

  1. React does not execute your callback
  2. React only stores the function reference and the dependencies
  3. The stored pair lives on the current Hook node

So on the first render, useCallback is basically a write:

Save this function and its dependency list for this Hook position.

3. What happens on updates: React compares dependencies and chooses old or new

The update phase is where useCallback becomes useful.

When the component renders again, React already has a previous Hook node for this call site. That node contains the old [callback, deps] pair from the last render.

Now React gets:

  • the newly created callback from the current render
  • the new dependency array
  • the previous callback and previous dependencies from the Hook node

Then it performs a dependency comparison:

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];

      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }

  hook.memoizedState = [callback, nextDeps];
  return callback;
}

The comparison itself is shallow and element by element. React does not compare the dependency array by reference alone. Instead, it checks each item with Object.is.

That means React is not doing a deep comparison for you. If one dependency is an object, array, or function whose reference changes every render, useCallback will also produce a new callback every render.

That leads to the real behavior:

  • If every dependency is the same, React returns the old callback reference
  • If any dependency changed, React stores and returns the new callback

The subtle but important point is timing:

By the time React compares dependencies, the new function already exists. useCallback may simply decide not to keep using it.

There is also one easy-to-miss rule:

  • If you omit the dependency array, React cannot do a stable comparison, so the callback is treated as new every render

Here is a quick way to think about the most common cases:

Call shapeResult
useCallback(fn)new function reference on every render
useCallback(fn, [])same function reference after mount, but it keeps the closure from the render where it was created
useCallback(fn, [a, b])same function reference until a or b changes by Object.is

4. Why useCallback is basically useMemo for functions

From a conceptual point of view, useCallback is just a specialized form of useMemo.

You can think of it like this:

function useCallback(fn, deps) {
  return useMemo(() => fn, deps);
}

That is not meant as a userland reimplementation, but it captures the key idea correctly:

  • useMemo caches the result of running a function
  • useCallback caches the function reference itself

Here is the difference in one table:

HookWhat gets storedWhat gets returned
useMemo(factory, deps)[computedValue, deps]the memoized value
useCallback(fn, deps)[fn, deps]the memoized function reference

That is also why these two Hooks share the same dependency logic. If the dependencies are unchanged, React reuses what it already has. The only difference is what kind of thing is being reused.

So if someone says:

"useCallback is basically useMemo but for functions"

that is a very solid mental model.

5. Where the memory comes from: all Hooks reuse Fiber Hook storage

The bigger question is not only "how does useCallback compare deps?" It is also:

Why can any Hook remember anything across renders in the first place?

The answer lives in React's Fiber architecture.

Each function component has a Fiber node, and that Fiber keeps Hook state attached to it. Conceptually, Hooks are stored as a linked list hanging off the Fiber's memoizedState.

For example:

Fiber.memoizedState
  -> [Hook 1: useState]
  -> [Hook 2: useCallback]
  -> [Hook 3: useEffect]
  -> null

Each Hook call in your component occupies one position in that list. On the first render, React builds the list. On later renders, React walks it again in the same order and reads or updates the node for each Hook call.

That is why the Rules of Hooks matter so much. If Hook order changes, React can no longer match "the second Hook call in this render" with "the second Hook node stored on the Fiber." The whole mapping breaks. If you want that piece in detail, see why Hooks cannot run in conditions, loops, or nested functions.

For useCallback, the Hook node stores [callback, deps]. For useMemo, it stores [value, deps]. For useState, React stores the current state plus its update queue.

The exact payload differs, but the deeper pattern is shared:

Hooks have memory because their per-call-site data is attached to a persistent Fiber across renders.

That is the real reason useCallback can return an old function reference at all.

6. What useCallback actually helps with in real code

This is the practical side many explanations skip.

useCallback is not a general "make the component faster" switch. It only helps when function identity itself matters.

The most common case is passing a callback to a memoized child:

import { memo, useCallback, useState } from "react";

const Child = memo(function Child({ onBuy }) {
  console.log("Child render");
  return <button onClick={onBuy}>Buy now</button>;
});

export default function ProductPage({ productId }) {
  const [count, setCount] = useState(0);

  const handleBuy = useCallback(() => {
    console.log("buy", productId);
  }, [productId]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <Child onBuy={handleBuy} />
    </div>
  );
}

Why can this matter?

  • The parent still re-renders when count changes
  • But if productId is unchanged, handleBuy can keep the same reference
  • That gives React.memo a chance to treat onBuy as unchanged and skip re-rendering the child

This same reference stability can also matter when:

  • a function is included in another Hook's dependency array
  • a custom Hook expects a stable callback
  • memoization logic depends on prop identity

But if none of those things are true, useCallback often adds ceremony without giving much back.

7. What useCallback does not solve

Because useCallback is frequently over-applied, it helps to be explicit about its limits.

It does not fix stale closures by itself

This is one of the biggest misunderstandings.

If you write this:

const handleSave = useCallback(() => {
  console.log(formValue);
}, []);

the callback reference is stable, but the closure is also stuck with whatever formValue meant in the render where that callback was created.

So a stable function is not automatically a fresh function. If you want the full mental model, see React stale closure: why your callback sees old state and how to fix it.

It does not stop the parent from re-rendering

useCallback can help a child skip rendering when prop identity matters. It does not stop the component that owns the Hook from rendering again.

It does not make the callback body cheaper to execute

useCallback memoizes the function reference. It does not memoize the work that happens when the function is called.

If the expensive part is computing a value during render, that points more toward useMemo. If the expensive part happens after a click, then useCallback alone is not the relevant optimization.

8. A clean mental model to keep

If you want one short explanation to remember, use this:

  1. Every render creates a new callback in JavaScript
  2. useCallback stores [callback, deps] on the current Hook node
  3. On the next render, React compares dependencies with Object.is
  4. If the dependencies are unchanged, React returns the older stored function reference
  5. That memory exists because Hook state lives on the component's Fiber across renders

So the real story is not "React magically freezes my function." It is:

React keeps Hook data on Fiber, and useCallback uses that storage to decide whether the previous function reference is still reusable.

That is also why useCallback, useMemo, and other Hooks feel so similar internally. They are not independent memory systems. They are different APIs built on the same underlying Hook storage model.

Once you see useCallback that way, it stops feeling like a special optimization trick and starts looking like a very small rule:

Reuse the old function reference when the dependency snapshot is still the same.

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-17

Contact the editor