React useCallback Hook

React useCallback Hook

The useCallback hook is an advanced performance optimization Hook. It returns a memoized (cached) version of a callback function that only changes if one of its dependencies has changed.

To understand why this is useful, we must understand how React renders components and how JavaScript evaluates function equality.


The Problem: Referential Equality

In React, whenever a component re-renders, every function defined inside that component is recreated.

In JavaScript, two functions are never equal, even if they have the exact same code.

const func1 = () => console.log('Hi');
const func2 = () => console.log('Hi');
console.log(func1 === func2); // Returns FALSE

If a parent component passes a callback function as a prop to a child component, the child component will see that function as a brand new prop on every render, causing the child to unnecessarily re-render (even if you wrapped the child in React.memo).


The Solution: useCallback

By wrapping a function in useCallback, you tell React to save (memoize) that function definition between renders. React will only recreate the function if the dependencies in the dependency array change.

Syntax

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // Dependency array
);

Example Without Optimization

Consider this scenario where typing in an input forces a heavy list component to re-render, making the app sluggish:

import React, { useState } from 'react';
import ExpensiveList from './ExpensiveList';

function App() { const [text, setText] = useState(''); const [items, setItems] = useState(['Apple', 'Banana', 'Cherry']);

// This function is recreated every time we type in the input! const removeItem = (itemToRemove) => { setItems(items.filter(item => item !== itemToRemove)); };

return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} /> {/* Because removeItem is re-created, ExpensiveList re-renders every keystroke! */} <ExpensiveList items={items} onRemove={removeItem} /> </div> ); }

Optimized Example with useCallback

To fix this, we wrap the removeItem function in useCallback and wrap the child component in React.memo (which prevents a component from re-rendering if its props haven't changed).

import React, { useState, useCallback } from 'react';
// Assume ExpensiveList is wrapped in React.memo()
import ExpensiveList from './ExpensiveList';

function App() { const [text, setText] = useState(''); const [items, setItems] = useState(['Apple', 'Banana', 'Cherry']);

// React caches this function. It only changes if 'items' changes. const removeItem = useCallback((itemToRemove) => { setItems(items.filter(item => item !== itemToRemove)); }, [items]); // Dependencies

return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} /> {/* Typing in the input no longer causes ExpensiveList to re-render! */} <ExpensiveList items={items} onRemove={removeItem} /> </div> ); }

Now, when you type in the text input, the App component re-renders, but useCallback returns the exact same removeItem function as before. ExpensiveList sees that its props haven't changed, and skips rendering!


When Should You NOT Use useCallback?

You shouldn't wrap every function in useCallback. Caching functions requires memory and computation.

Only use useCallback when:

  1. You are passing the function as a prop to a child component that is wrapped in React.memo.
  2. The function is used as a dependency in another Hook (like useEffect or useMemo).

For simple components, the cost of re-creating the function is lower than the cost of running the useCallback logic.


Reminder: If your callback uses state variables or props from the component scope, you must include them in the dependency array. Otherwise, the cached function will execute with stale, outdated data.


Exercise

?

What is the primary purpose of the useCallback Hook?