ReactuseCallback: Memoizing Functions

useCallback: Memoizing Functions

Every time a React component renders, all the functions defined inside it are recreated — new references are allocated in memory. For most props this is invisible. But when a function is passed to a memoized child component, or used as a dependency in useEffect, a new function reference on every render causes those children to re-render or effects to re-run unnecessarily. useCallback solves this by returning a stable, memoized function reference.

The Signature

JSX
const memoizedFn = useCallback(
  function fn(args) {
    // function body
  },
  [deps] // dependency array — same as useEffect
)
  • On the first render, useCallback stores the function and returns it

  • On subsequent renders, if the dependencies have not changed, React returns the same function reference

  • When a dependency changes, React creates and stores a new function

The Problem: Functions Break React.memo

React.memo wraps a component and makes it skip re-rendering if its props are identical (shallow comparison). This works perfectly for primitive props. But for function props, every render creates a new function — a new reference — so React.memo always sees a "changed" prop and re-renders.

JSX
import { useState, memo } from 'react'

// Expensive child — wrapped with React.memo
const ExpensiveButton = memo(function ExpensiveButton({ onClick, label }) {
  console.log('ExpensiveButton rendered:', label)
  return <button onClick={onClick}>{label}</button>
})

// ❌ Without useCallback — ExpensiveButton re-renders on every parent render
function Parent() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

  // New function reference on every render
  const handleClick = () => {
    setCount(c => c + 1)
  }

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <p>Count: {count}</p>
      {/* Every keystroke in the input re-renders Parent, which creates a new
          handleClick → React.memo sees a new prop → ExpensiveButton re-renders */}
      <ExpensiveButton onClick={handleClick} label="Increment" />
    </div>
  )
}

JSX
import { useState, useCallback, memo } from 'react'

const ExpensiveButton = memo(function ExpensiveButton({ onClick, label }) {
  console.log('ExpensiveButton rendered:', label)
  return <button onClick={onClick}>{label}</button>
})

// ✅ With useCallback — ExpensiveButton only re-renders when handleClick changes
function Parent() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

  // Stable reference — same function as long as deps don't change
  const handleClick = useCallback(() => {
    setCount(c => c + 1)
  }, []) // no deps: setCount is stable, and we use the functional updater form

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <p>Count: {count}</p>
      {/* Now typing in the input does NOT re-render ExpensiveButton */}
      <ExpensiveButton onClick={handleClick} label="Increment" />
    </div>
  )
}
Note
`useCallback` is only valuable when the function is passed to a component wrapped with `React.memo`, or when it is used as a `useEffect` dependency. If neither applies, the overhead of `useCallback` outweighs the benefit.
Use Case 2: Function as a useEffect Dependency

When a function is listed in a useEffect dependency array, every re-render that creates a new function reference will re-run the effect. useCallback stabilizes the reference.

JSX
import { useState, useCallback, useEffect } from 'react'

// ❌ Without useCallback — effect re-runs on every render
function DataFetcher({ endpoint }) {
  const [data, setData] = useState(null)

  // New function every render
  const fetchData = async () => {
    const res = await fetch(endpoint)
    setData(await res.json())
  }

  // Because fetchData is a new reference every render, this effect
  // re-runs continuously — infinite loop!
  useEffect(() => {
    fetchData()
  }, [fetchData]) // ← new reference every render

  return <div>{JSON.stringify(data)}</div>
}

// ✅ With useCallback — effect only re-runs when endpoint changes
function DataFetcher({ endpoint }) {
  const [data, setData] = useState(null)

  const fetchData = useCallback(async () => {
    const res = await fetch(endpoint)
    setData(await res.json())
  }, [endpoint]) // stable when endpoint is stable

  useEffect(() => {
    fetchData()
  }, [fetchData]) // only re-runs when fetchData (= endpoint) changes

  return <div>{JSON.stringify(data)}</div>
}
Use Case 3: Ref Callback for DOM Measurement

A ref callback is a function passed to the ref prop. React calls it with the DOM node when it mounts, and with null when it unmounts. Without useCallback, a new function reference on every render causes the ref callback to fire on every render — which is usually not what you want.

JSX
import { useState, useCallback } from 'react'

function MeasuredBox() {
  const [height, setHeight] = useState(0)

  // useCallback ensures this is the same function — React only calls
  // the ref callback when the node mounts/unmounts, not on every render.
  const measuredRef = useCallback((node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, []) // no deps — only needs to run when the element mounts

  return (
    <div>
      <div ref={measuredRef} style={{ padding: 20 }}>
        <p>Resize the window to see changes</p>
      </div>
      <p>Box height: {height}px</p>
    </div>
  )
}
Before and After: A Complete Example

A TodoList with a memoized row component. Without useCallback, every interaction causes all rows to re-render.

JSX
import { useState, useCallback, memo } from 'react'

// Memoized row — only re-renders when its own props change
const TodoRow = memo(function TodoRow({ todo, onToggle, onDelete }) {
  console.log('Rendering row:', todo.id)
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  )
})

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Write tests', done: false },
    { id: 3, text: 'Deploy app', done: false },
  ])
  const [newText, setNewText] = useState('')

  // Stable references — handlers only change if setTodos changes (it never does)
  const handleToggle = useCallback((id) => {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
  }, [])

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(t => t.id !== id))
  }, [])

  function handleAdd() {
    if (!newText.trim()) return
    setTodos(prev => [...prev, { id: Date.now(), text: newText, done: false }])
    setNewText('')
  }

  return (
    <div>
      <input value={newText} onChange={e => setNewText(e.target.value)} />
      <button onClick={handleAdd}>Add</button>
      <ul>
        {todos.map(todo => (
          <TodoRow
            key={todo.id}
            todo={todo}
            onToggle={handleToggle}
            onDelete={handleDelete}
          />
        ))}
      </ul>
    </div>
  )
}

Typing in the "add" input changes newText, which re-renders TodoList. Without useCallback, handleToggle and handleDelete get new references → all TodoRow components re-render. With useCallback, the handlers are stable → only the specific row that changed would need to re-render.

When NOT to Use useCallback
Warning
`useCallback` adds overhead: React must store the function, compare deps on every render, and manage the cache. If the child is not memoized with `React.memo`, `useCallback` provides zero benefit — the child will re-render regardless. Measure before adding it.

Scenario

Use useCallback?

Function passed to React.memo child

Yes — necessary for memo to work

Function in useEffect deps array

Yes — prevents infinite re-runs

Ref callback (DOM measurement)

Yes — prevents extra ref calls

Inline handler not passed to memo child

No — overhead not worth it

Function only used in the same component

No — not passed anywhere

Tip
A common pattern: use `useCallback` only after you have wrapped the receiving child with `React.memo`. Without `React.memo`, the child re-renders regardless of prop stability — `useCallback` is wasted effort on its own.