engineering

What is useCallback Hook in React?

What is useCallback Hook in React?

Learn everything about the useCallback hook in React - memoization, syntax, use cases, examples, useCallback vs useMemo, interview questions - in one article.

Kaushal Joshi

Kaushal Joshi

Feb 13, 2025 13 min read

When I started learning React, I was quite confident at first. Everything I was learning— component rerendering, state and props, basic hooks, etc.— seemed simple at first. Until one day, I came across memoization. That day, I felt like I was stuck in my React journey.

Optimization becomes an important part of the application if you are building a large-scale application with complex logic, data flow, and user interaction. In terms of React, this means controlling what components get rerendered and whether you can prevent it. React provides a built-in hook, useCallback(), that memoizes a function, preventing unnecessary recreation and reducing component re-renders, thereby improving performance.

In this blog, let's dive deep into one of the most important hooks in React - useCallback. We will start from the basics and learn about memoization, and different memoization methods in React, and then learn about the useCallback hook. We'd learn what it is, how to write it, when to use it, and how to use it, and end our discussion with some interview questions.

Prerequisites and Assumptions

The useCallback() hook is an advanced hook in React.js. Hence, I assume you're well familiar with basic React concepts like functional components, states, props, etc. If you are just getting started with React, I'd highly recommend saving this article for now and coming back to it later.

That being said, let's start with knowing what is memoization.

What is Memoization?

Memoization is an optimization technique that improves performance by caching previously computed results. When the function is called with the same inputs, it returns the cached results instead of recomputing them.

This works because of closures in JavaScript. Closures allow a function to "remember" the variables from its outer scope even after the outer function has finished executing. In the context of memoization, this means the cached results persist across multiple function calls, ensuring we don’t recompute values unnecessarily.

Let's see a simple example of Memoization in JavaScript:

function memoizedAdd() {
  let cache = {};
  return function(a, b) {
    const key = `${a}-${b}`;
    if (cache[key]) return cache[key];

    cache[key] = a + b;
    return cache[key];
  };
}

const add = memoizedAdd();

console.log(add(2, 3)); // Computes and stores result
console.log(add(2, 3)); // Returns cached result

This is how it works:

  1. Check the cache: Check if the result for the given set of inputs exists in the cache.

  2. Compute if needed: If the result is not cached, compute and store it.

  3. Return cached result: When the function is called again with the same inputs, return the cached results instead of recomputing them.

Memoization is a general programming concept and is not limited to JavaScript, React, or front-end development.

Memoization in React

In React, memoization helps optimize performance by preventing unnecessary rerenders and recomputations. React provides built-in hooks like useMemo() and useCallback() and a memo() function that memoizes different parts of the component.

  1. memo(): If you wrap a functional component with memo(), the component will rerender only if the props are changed. Otherwise, it'd skip the rerender.

  2. useMemo(): This built-in hook caches the result of an expensive computation and only recalculates it when dependencies change.

  3. useCallback(): This built-in hook memoizes a function so it doesn’t get recreated on every render.

Now as our basics are clear, let's see what the useCallback() hook is all about.

What is the useCallback Hook in React?

The useCallback hook in React memoizes a function, ensuring that its reference remains stable across renders unless its dependencies change. This helps optimize performance by preventing unnecessary recreations of the function, which results in the rendering of the components.

Parameters of useCallback

  1. fn: This is the function that you want to memoize. The function could be any valid JavaScript function.

  2. dependencies: This is an array of all reactive values referenced inside of the function passed as the first parameter. Reactive values include props, states, context values, and all other variables and functions declared directly inside your component body. Similar to useEffect, you should only add those values that are being used inside the function.

The return value of useCallback

  1. On the initial render, the useCallback hook returns the function passed as the first parameter itself.

  2. During the next render, it will first check if the dependencies have changed. If so, it will return a new function based on new values.

  3. If dependencies are not changed, it will return the already stored function from the last render.

The useCallback hook does not call the function. It simply stores ("caches") the function definition and returns the same function reference unless dependencies change.

Caveats with useCallback

  1. It's crucial to add a dependency array to the useCallback hook. The values inside the list decide when the function should be recreated. If you miss a value, the function won't be recreated. On the other hand, if you add additional values, the function will be recreated unnecessarily.

  2. If you have passed an empty dependency array, the function will be frozen on the first render itself. On all future renders, the same function from the first render will be reused.

First useCallback example

Let's see a simple example to understand useCallback hook.

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

const Button = memo(({ onClick }) => {
  return <button onClick={onClick}>Increment count</button>;
});

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

  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={increment} />
    </div>
  )
}

Let's see what the code does...

  1. The <Button /> component is wrapped in memo(). That means it'd not rerender if the props are unchanged.

  2. We have wrapped increment() inside useCallback with an empty dependency array. This ensures that the function reference remains the same across re-renders, preventing unnecessary recreation.

  3. We pass the increment() function to <Button /> component as a prop. Here, we ensure that the button component doesn't rerender when the count prop changes.

Real World Example of useCallback

Now, let's see a real-world example of the useCallback hook where memoizing a function makes sense.

Imagine you have a large list of users and a search input that filters the list. A separate component <UserList /> takes the list of filtered users and renders them on the screen.

import { useState } from "react";

const users = ["Alice", "Bob", "Charlie", "David", "Eve"];

function UserList({ filter }: { filter: (user: string) => boolean }) {
  console.log("UserList rerendered"); //  Rerenders every time

  return (
    <ul>
      {users.filter(filter).map((user) => (
        <li key={user}>{user}</li>
      ))}
    </ul>
  );
}

export default function App() {
  const [searchQuery, setSearchQuery] = useState("");

  //  filterUsers is recreated on every render
  const filteredUsers = (user: string) => user.toLowerCase().includes(searchQuery.toLowerCase());

  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
      />
      <UserList filter={filteredUsers} />
    </div>
  );
}

The filteredUsers() should be optimized because it is passed down to a <UserList /> component. If we don't use useCallback, the filtering function will be recreated on every render, causing <UserList /> to rerender unnecessarily.

Let's optimize this snippet by ensuring that filteredUsers only changes when searchQuery updates. Additionally, we'll wrap <UserList /> with memo() so it re-renders only when its filter prop changes.

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

const users = ["Alice", "Bob", "Charlie", "David", "Eve"];

// Memoized UserList to prevent unnecessary renders
const UserList = memo(({ filter }: { filter: (user: string) => boolean }) => {
  console.log("UserList rerendered"); // Now only rerenders when filter changes

  return (
    <ul>
      {users.filter(filter).map((user) => (
        <li key={user}>{user}</li>
      ))}
    </ul>
  );
});

export default function App() {
  const [searchQuery, setSearchQuery] = useState("");

  // Memoized function, only changes when searchQuery changes
  const filteredUsers = useCallback(
    (user: string) => user.toLowerCase().includes(searchQuery.toLowerCase()),
    [searchQuery]
  );

  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
      />
      <UserList filter={filteredUsers} />
    </div>
  );
}

Let's understand this.

  1. filteredUsers is not recreated unless searchQuery changes.

  2. As <UserList /> is wrapped in memo(), it will only rerender if filteredUsers changes.

  3. If the user types quickly, useCallback prevents frequent rerenders of <UserList />, improving efficiency.

Why Functions Are Recreated During Every Render?

By now, you might be wondering: why are functions recreated on every render? Can't React simply skip recreating them? The answer lies in how JavaScript handles functions within components.

All modern React components are functions. When a component renders, it is simply a function call. This means that every time React rerenders a component, it calls the function again from scratch.

Functions declared inside a component are recreated on every render because they exist within that specific function's execution context (scope). Since components in React are just JavaScript functions, they re-run entirely upon re-render, leading to function recreation unless memoized.

Therefore, even if you check prevFunction === newFunction, it will return false because each function instance has a different memory reference.

This is why React treats the new function as different from the previous one, even if their implementation is identical.

How does useCallback caches a function?

When a component renders, useCallback creates, and stores a function reference in React's internal cache. This reference points to the function's memory address.

On subsequent renders, useCallback checks the dependency array:

  • If the dependency array is empty ([]), or its values haven't changed, React returns the previously cached function reference, avoiding unnecessary recreation.

  • If any dependency has changed, useCallback creates a new function and updates the cache, replacing the old reference.

This mechanism prevents redundant function recreations, improving performance, especially when passing functions to child components that rely on reference equality (useEffect, useMemo, React.memo).

Use Cases of useCallback

Preventing unnecessary rerenders of child components

This is the most important use case of the useCallback hook. We saw why this is important in the previous section of this article. Let's see another example of the same.

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

const Button = memo(({ onClick }) => {
  console.log("button rerendered...");

  return <button onClick={onClick}>Click me</button>;
});

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

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={handleClick} />
    </div>
  );
}
  1. The handleClick function is wrapped inside useCallback with an empty dependency array. This makes sure that the function is only created during the first render.

  2. The <Button /> component is wrapped with memo(), making sure it won't rerender unless onChange has changed.

  3. This would ensure that <Button /> renders only once, and does not rerender unnecessarily.

An important thing to note here is that you should wrap the child component with memo(). Otherwise, the component will rerender even if the prop hasn't changed.

Preventing an effect from firing too soon

Sometimes, you might want to call a function inside an effect. Hence you must add it in its dependency array. This creates a problem because the function will be created every time the component rerenders. And hence, the effect will be triggered on every render as well.

import { useState, useEffect } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  const logCount = () => {
    console.log(`Count: ${count}`);
  };

  useEffect(() => {
    logCount();
  }, [logCount]); // logCount changes on every render

  return (
    <button onClick={() => setCount(count + 1)}>
      Increment {count}
    </button>
  );
}

To resolve this, you can wrap the function in useCallback. This ensures that the function only recreates when the dependency changes. Now, the effect doesn't run on every render. It'd only run when the function changes.

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

function Counter() {
  const [count, setCount] = useState(0);

  const logCount = useCallback(() => {
    console.log(`Count: ${count}`);
  }, [count]); // Only recreates when `count` changes

  useEffect(() => {
    logCount();
  }, [logCount]);

  return (
    <button onClick={() => setCount(count + 1)}>
      Increment {count}
    </button>
  );
}

Memoizing a custom hook

If you are writing a custom hook, you should always wrap all the functions returned by the hook inside useCallback. Consumer components can optimize their code whenever needed.

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}

useCallback vs useMemo

We briefly saw another hook called useMemo in one of the first sections of this article. Now you must be wondering what's the difference between these two.

The main difference between useCallback and useMemo is that useMemo caches the result of calling a function whereas useCallback caches the function itself.

useMemo calls the function whenever values in the dependency list updates, and stores the result. useCallback does not call the function you provide. It caches the function you provide so that the function itself doesn't change unless the dependencies are changed. With useCallback, you still need to call the function to get a result.

import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
  const product = useData('/product/' + productId);

  const requirements = useMemo(() => { // Calls your function and caches its result
    return computeRequirements(product);
  }, [product]);

  const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
    </div>
  );
}

Should You Use useCallback Everywhere?

The short answer is no, you should not.

It's unnecessary to use memorization if your app consists of navigating through different pages and replacing entire sections of a page often. If it's more like a single-page application where the user hardly goes to another page (or does it less often), then you should consider memoization.

Memozation is only useful in two cases:

  1. You pass a function as a prop to a component wrapped in memo().

  2. The function is being used as a dependency of some hook.

Apart from these, there are no fancy advantages of using useCallback. Wrapping all functions inside useCallback reduces the code readability. And even if any one dependency from the useCallback changes, it'd cause the entire component to rerender.

Therefore, you should focus on writing optimized code that requires little to no memoization.

  1. When a component wraps other components / HTML elements, accept JSX as children.

  2. Prefer local states and avoid lifting states to parent components unless necessary.

  3. Avoid unnecessary effects that update the state.

  4. Remove unnecessary dependencies and try to use as few dependencies in your effects as possible.

Interview Questions for useCallback

By now you must be very confident in memoization in React and the useCallback hook. If you're preparing for mid to senior-level frontend interviews, useCallback is a hot topic amongst interviewers to test your React knowledge. Here are some interview questions for you to test what you learned today:

  1. Why does React recreate functions on every render? Is it normal?

  2. What is useCallback hook? How does it improve performance?

  3. Difference between useCallback, useMemo, and memo.

  4. Can you use useMemo to memoize a function? Write code and explain.

  5. What would happen if you don't provide a dependency array to useCallback?

  6. Should you wrap all functions in useCallback?

  7. How would you optimize a React app without using useCallback, useMemo, and memo?

Wrapping Up

Today we learned about the useCallback hook in React. It is one of the advanced hooks in React. So it's alright if you didn't fully understand it at first read. Take a break, come back, read again, write some code, and I am sure you'll understand it better.

I hope you found this article helpful. If you did, feel free to share it with your friends and peers. I have written deep technical articles on other React-focused topics that'd help you prepare better.

  1. What is forwardRef in React?

  2. useRef Hook in React

  3. What Are Portals in React?

  4. What is useLayoutEffect in React?

If you are looking for a new job or want to share a cool project you built with likeminded folks, checkout Peerlist!

All the best for your interviews. Until then, keep grinding.

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.