🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
REFERENCEreact

react Documentation

LOADING ENGINE...

useCallback

AI & DATA SCIENCE // usecallback

useCallback memoizes a function definition itself between renders, returning the same function reference as long as its listed dependencies haven't changed.

Syntax

const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

Deep Dive Course

Every time a component re-renders, any function defined directly inside it, including inline event handlers, is recreated as a brand-new function value, even if its logic is identical to the previous render's version — this normally doesn't matter, but it does specifically when that function is passed as a prop to a child wrapped in React.memo(), since a new function reference on every render defeats that memoization, causing the child to re-render anyway despite React.memo()'s comparison. useCallback(fn, deps) returns the exact same function reference across renders as long as the dependency array's values haven't changed, preserving that memoized child's optimization.

1Understanding useCallback

Every time a component re-renders, any function defined directly inside it, including inline event handlers, is recreated as a brand-new function value, even if its logic is identical to the previous render's version — this normally doesn't matter, but it does specifically when that function is passed as a prop to a child wrapped in React.memo(), since a new function reference on every render defeats that memoization, causing the child to re-render anyway despite React.memo()'s comparison. useCallback(fn, deps) returns the exact same function reference across renders as long as the dependency array's values haven't changed, preserving that memoized child's optimization.

💡

useCallback only matters when a function's reference identity actually matters somewhere downstream, most commonly as a prop to a React.memo()-wrapped child, or as a dependency in another hook's dependency array — wrapping every function in a component with useCallback by default adds overhead without any real benefit in most other cases.

editor.html
import { useCallback, useState } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment: {count}</button>
      <MemoizedChild onClick={handleClick} />
    </>
  );
}
localhost:3000

2Practical Example

Here is a real-world application of useCallback showing how it is used in production React code.

editor.html
const MemoizedChild = React.memo(function Child({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Child Button</button>;
});
localhost:3000

3Best Practices

Follow these guidelines when working with useCallback:

1. Use useCallback specifically for functions passed as props to a React.memo()-wrapped child, so a new reference on every parent render doesn't defeat that child's memoization

2. Use useCallback for a function that's itself listed as a dependency in another hook, like useEffect, to avoid that effect re-running on every render due to a constantly-new function reference

3. Avoid wrapping every function in a component with useCallback by default, since the memoization bookkeeping itself has a cost that isn't worth paying when nothing downstream actually relies on a stable reference

⚠️

Tip: useCallback only matters when a function's reference identity actually matters somewhere downstream, most commonly as a prop to a React.memo()-wrapped child, or as a dependency in another hook's dependency array — wrapping every function in a component with useCallback by default adds overhead without any real benefit in most other cases.

editor.html
import { useCallback, useState } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment: {count}</button>
      <MemoizedChild onClick={handleClick} />
    </>
  );
}
localhost:3000

4Stale Closures Inside useCallback

A memoized function is only recreated when its dependency array changes, so any value it reads from component scope but doesn't list as a dependency stays frozen at whatever it was when the current memoized version was created. An empty dependency array permanently traps the values from the very first render — the same stale-closure trap that affects useEffect.

⚠️

An empty dependency array on useCallback doesn't mean 'always up to date' — it means 'frozen at what this closure saw on the render that created it.'

editor.html
// ❌ Traps count's initial value (0) forever
const logCount = useCallback(() => {
  console.log(count);
}, []);

// ✅ Reads the current count every time it's recreated
const logCount = useCallback(() => {
  console.log(count);
}, [count]);
localhost:3000

5Feeding a Stable Callback Into useEffect

When a function is used both inside an effect and elsewhere (like an event handler), wrapping it in useCallback lets you safely list it as an effect dependency without the effect re-running on every render — since the function's reference only changes when its own dependencies actually change.

editor.html
const fetchData = useCallback(() => {
  api.get(`/items/${id}`).then(setItems);
}, [id]);

useEffect(() => {
  fetchData();
}, [fetchData]);
localhost:3000

Examples

Example 01Basic Usage
import { useCallback, useState } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment: {count}</button>
      <MemoizedChild onClick={handleClick} />
    </>
  );
}
Example 02Advanced Example
const MemoizedChild = React.memo(function Child({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Child Button</button>;
});
Example 03Using a Memoized Callback as an Effect Dependency
import { useCallback, useEffect, useState } from 'react';

function Items({ id }) {
  const [items, setItems] = useState([]);
  const fetchData = useCallback(() => {
    api.get(`/items/${id}`).then(setItems);
  }, [id]);
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Best Practices

  • Use useCallback specifically for functions passed as props to a React.memo()-wrapped child, so a new reference on every parent render doesn't defeat that child's memoization
  • Use useCallback for a function that's itself listed as a dependency in another hook, like useEffect, to avoid that effect re-running on every render due to a constantly-new function reference
  • Avoid wrapping every function in a component with useCallback by default, since the memoization bookkeeping itself has a cost that isn't worth paying when nothing downstream actually relies on a stable reference
  • Include every value the memoized function's body actually reads in its dependency array, or it will keep executing with a stale, outdated version of that value
  • Remember useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) — reach for useMemo instead when memoizing a computed value rather than a function

Interview Question

Why does passing an inline arrow function, like onClick={() => doSomething()}, as a prop to a React.memo()-wrapped child defeat that child's memoization, even if the function's logic never changes?

Hint: Think about whether two functions with identical code are considered the 'same' value by JavaScript's equality comparison.

React.memo()'s optimization works by comparing each new prop against its previous value using a shallow equality check, essentially === for most values, and in JavaScript, two separately created functions are never considered === to each other even if their code is byte-for-byte identical, since each function definition creates a genuinely new, distinct function object in memory. Writing an inline arrow function directly in JSX means a brand-new function object gets created on every single render of the parent, so even though the child's logic never changes, the prop's reference is different every time, causing React.memo()'s shallow comparison to see the onClick prop as changed and re-render the child anyway. useCallback fixes this by returning the exact same function reference across renders, as long as its dependencies haven't changed, so the prop passed down genuinely stays === to its previous value, letting React.memo()'s comparison correctly recognize that nothing relevant actually changed and skip that child's re-render.

Exercises

MediumPractice using useCallback in a real scenario.
View Solution
import { useCallback, useState } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment: {count}</button>
      <MemoizedChild onClick={handleClick} />
    </>
  );
}
HardThis useCallback always logs the count from the first render because of a stale closure. Fix the dependency array.
View Solution
// Wrong
// const logCount = useCallback(() => console.log(count), []);

// Correct
import { useCallback, useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const logCount = useCallback(() => {
    console.log(count);
  }, [count]);
  return <button onClick={() => { setCount(c => c + 1); logCount(); }}>{count}</button>;
}

Frequently Asked Questions

Why does passing an inline arrow function, like onClick={() => doSomething()}, as a prop to a React.memo()-wrapped child defeat that child's memoization, even if the function's logic never changes?

React.memo()'s optimization works by comparing each new prop against its previous value using a shallow equality check, essentially === for most values, and in JavaScript, two separately created functions are never considered === to each other even if their code is byte-for-byte identical, since each function definition creates a genuinely new, distinct function object in memory. Writing an inline arrow function directly in JSX means a brand-new function object gets created on every single render of the parent, so even though the child's logic never changes, the prop's reference is different every time, causing React.memo()'s shallow comparison to see the onClick prop as changed and re-render the child anyway. useCallback fixes this by returning the exact same function reference across renders, as long as its dependencies haven't changed, so the prop passed down genuinely stays === to its previous value, letting React.memo()'s comparison correctly recognize that nothing relevant actually changed and skip that child's re-render.

Why does a useCallback with an empty dependency array keep logging an outdated value of a state variable it reads?

useCallback only recreates the memoized function when a value in its dependency array changes — with an empty array, that means the function is created exactly once, on the first render, and every subsequent render reuses that exact same function object rather than creating a fresh one. A JavaScript function closes over the variables from the scope where it was defined, so that first-render function permanently captured whatever the state variable's value was at that moment; later renders never get the chance to update what that already-created function 'sees', since it's never recreated. Adding the state variable to the dependency array fixes this by forcing useCallback to produce a new function, closing over the current value, every time that variable actually changes.

Related Functions

usememocomponent-renderingusestate