useCallback & Function Stability

Med

useCallback returns a memoized version of a callback function that only changes when its dependencies change. It is primarily useful when passing callbacks to memoized child components or when functions appear in effect dependency arrays, preventing unnecessary re-renders or effect re-runs.

Interactive Visualization

Key Points

  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
  • Without useCallback, a new function reference is created every render
  • Primary use: prevent re-renders of React.memo children receiving callback props
  • Secondary use: stabilize functions used in useEffect dependency arrays
  • Only beneficial when paired with React.memo or dependency arrays
  • Adds overhead from closure creation and dependency comparison

Code Examples

Preventing Child Re-renders

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

interface ItemProps {
  id: string
  name: string
  onDelete: (id: string) => void
}

const ExpensiveItem = memo(function ExpensiveItem({
  id,
  name,
  onDelete,
}: ItemProps) {
  return (
    <div>
      <span>{name}</span>
      <button onClick={() => onDelete(id)}>Delete</button>
    </div>
  )
})

function ItemList({ items }: { items: { id: string; name: string }[] }) {
  const [selected, setSelected] = useState<string | null>(null)

  // Without useCallback, onDelete changes every render,
  // defeating React.memo on ExpensiveItem
  const handleDelete = useCallback((id: string) => {
    // delete logic here
  }, [])

  return (
    <div>
      <p>Selected: {selected}</p>
      {items.map((item) => (
        <ExpensiveItem
          key={item.id}
          id={item.id}
          name={item.name}
          onDelete={handleDelete}
        />
      ))}
    </div>
  )
}

useCallback keeps the same function reference so ExpensiveItem wrapped in React.memo can skip re-rendering when selected changes

Stable Function in Effect Dependencies

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

function SearchResults({ query }: { query: string }) {
  const [results, setResults] = useState<string[]>([])

  const fetchResults = useCallback(async () => {
    const res = await fetch(`/api/search?q=${query}`)
    const data = await res.json() as string[]
    setResults(data)
  }, [query])

  useEffect(() => {
    fetchResults()
  }, [fetchResults])

  return (
    <ul>
      {results.map((r, i) => <li key={i}>{r}</li>)}
    </ul>
  )
}

useCallback makes fetchResults change only when query changes, so the effect does not re-run on every render

Common Mistakes

  • Using useCallback on every function even when no child uses React.memo
  • Forgetting to include all used variables in the dependency array
  • Using useCallback without React.memo on children, providing no benefit

Interview Tips

  • Explain that useCallback without React.memo on children is pointless overhead
  • Know that useCallback(fn, deps) is syntactic sugar for useMemo(() => fn, deps)
  • Discuss the upcoming React Compiler which auto-memoizes and may make useCallback unnecessary

Related Concepts