DEV Community

Mohamed Idris
Mohamed Idris

Posted on

The useCallback react hook

Solving the Re-render Issue with useCallback Hook

In the previous example, we faced a problem where a memoized component was re-rendering unnecessarily because its function prop (removePerson) was being recreated each time the state changed. Let’s look at how we can fix this using the useCallback hook in React.


What is the useCallback Hook?

The useCallback hook helps us keep the same version of a function between re-renders unless its dependencies change. This is helpful when you have functions that don't need to be recreated on every render.

useCallback takes two arguments:

  1. The function you want to "remember" (in our case, removePerson).
  2. A list of things that can change (dependencies) — if any of these change, the function will be recreated. Otherwise, it will stay the same.

Recap of the Problem

Before we used useCallback, every time the count changed, the removePerson function was recreated. This caused the List component to re-render unnecessarily, even when the people list didn’t change.

How to Fix It with useCallback

We need to "remember" the removePerson function so it doesn't get recreated every time the state changes. Here's how we can do that:

Updated Code: Using useCallback to Fix the Issue

App.jsx (After Using useCallback)

import { useState, useCallback } from 'react';
import { data } from '../../../../data';
import List from './List';

const App = () => {
  const [people, setPeople] = useState(data);
  const [count, setCount] = useState(0);

  // Memorizing the function using useCallback
  const removePerson = useCallback(
    (id) => {
      const newPeople = people.filter((person) => person.id !== id);
      setPeople(newPeople);
    },
    [people]  // Only recreate the function if the people list changes
  );

  return (
    <section>
      <button
        className="btn"
        onClick={() => setCount(count + 1)}
        style={{ marginBottom: '1rem' }}
      >
        count {count}
      </button>
      <List people={people} removePerson={removePerson} />
    </section>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Key Changes:

  1. Using useCallback: We wrap the removePerson function in useCallback, so it only changes when the people list changes.
  2. Dependencies: We pass [people] as the dependency. This means the function will only be recreated when the people list changes, not when other state (like count) changes.

Why the Dependency Array is Important

When we use useCallback with an empty dependency array ([]), the function will be created only once, when the component first loads. However, this can cause problems. Since we are updating the people array (when we remove a person), if we don't include people in the dependency list, the function won't update with the new people array.

The Gotcha: What Happens If Dependencies Are Missing

If the dependency array is empty ([]), the function will always reference the initial people array, even after we remove a person. This can cause bugs, like when we try to remove someone from the list but nothing happens after the first removal.

Here’s the issue with an empty dependency array:

const removePerson = useCallback(
  (id) => {
    console.log(people, id);
    const newPeople = people.filter((person) => person.id !== id);
    setPeople(newPeople);
  },
  []  // No dependencies
);
Enter fullscreen mode Exit fullscreen mode

In this case, the function will always use the original people array, which doesn’t update after we remove a person. This is why we see strange behavior.

Console log


Fixing It: Correctly Using people as a Dependency

Now, if we pass [people] as the dependency, the function will be updated every time the people list changes, and everything will work as expected.

Here’s the working solution with useCallback:

const removePerson = useCallback(
  (id) => {
    const newPeople = people.filter((person) => person.id !== id);
    setPeople(newPeople);
  },
  [people]  // Now the function updates whenever the people list changes
);
Enter fullscreen mode Exit fullscreen mode

Visual Comparison: Before vs After useCallback

Before Using useCallback

Before Using useCallback

  • Every time we click the count button, the removePerson function is recreated, causing unnecessary re-renders.

After Using useCallback

After Using useCallback

  • Now, the removePerson function is only recreated when the people list changes, and unnecessary re-renders are prevented.

Conclusion

By using the useCallback hook:

  • We keep the same function reference unless the people array changes.
  • We reduce unnecessary re-renders, especially for components like List, where the people list is passed as a prop.
  • This makes our app more efficient, especially when dealing with functions that don’t need to be recreated every time the state changes.

This solution improves performance and keeps our app running smoothly! 😊

Credits: John Smilga's course

Top comments (1)

Collapse
 
edriso profile image
Mohamed Idris

Common use case: Solving the Re-render Issue with useCallback Hook

In React, using useEffect to fetch data is a common pattern. However, if you define a function inside useEffect (like fetchData), it will be recreated every time the component re-renders. This can cause issues when that function is included in the dependency array of useEffect, especially in Create React App projects, where you may encounter an ESLint warning about the function being re-created on every render.

The Problem

Let’s say you have a fetchData function inside useEffect that fetches data from an API. If you include fetchData in the dependency array of useEffect, it will cause the function to be recreated every time the component re-renders, even if nothing about the function has actually changed. This can lead to unnecessary re-fetching and other issues.

In Create React App, this will often result in an ESLint warning about missing dependencies or functions that change on every render.

How to Fix It with useCallback

To solve this problem, we can use the useCallback hook to memoize the fetchData function. By doing this, React will "remember" the function and only recreate it if its dependencies change.

Example: Fetching Data from an API

Before Using useCallback

Here’s the initial version of the code where fetchData is defined inside the useEffect hook:

import { useState, useEffect } from 'react';

const url = 'https://api.github.com/users';

const App = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const users = await response.json();
        setUsers(users);
      } catch (error) {
        console.log(error);
      }
    };
    fetchData();
  }, []);  // Empty dependency array, but the fetchData function is recreated on every render

  return (
    <section>
      <h3>Github Users</h3>
      <ul className="users">
        {users.map((user) => (
          <li key={user.id}>
            <img src={user.avatar_url} alt={user.login} />
            <div>
              <h5>{user.login}</h5>
              <a href={user.html_url}>Profile</a>
            </div>
          </li>
        ))}
      </ul>
    </section>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this setup, the fetchData function is defined inside useEffect. Each time the component re-renders, fetchData is recreated. While this works, it can lead to issues in projects like Create React App, where you might see ESLint warnings about the fetchData function being recreated every render.

After Using useCallback

Now, let’s fix this by memoizing the fetchData function using useCallback:

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

const url = 'https://api.github.com/users';

const App = () => {
  const [users, setUsers] = useState([]);

  // Use useCallback to memoize the fetchData function
  const fetchData = useCallback(async () => {
    try {
      const response = await fetch(url);
      const users = await response.json();
      setUsers(users);
    } catch (error) {
      console.log(error);
    }
  }, []);  // Empty dependency array: fetchData is only created once on initial render

  useEffect(() => {
    fetchData();  // Call the memoized fetchData function
  }, [fetchData]);  // Dependency array ensures fetchData is called only when it changes

  return (
    <section>
      <h3>Github Users</h3>
      <ul className="users">
        {users.map((user) => (
          <li key={user.id}>
            <img src={user.avatar_url} alt={user.login} />
            <div>
              <h5>{user.login}</h5>
              <a href={user.html_url}>Profile</a>
            </div>
          </li>
        ))}
      </ul>
    </section>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • useCallback: The fetchData function is now wrapped in the useCallback hook. This ensures that the function is only recreated if its dependencies change. Since we’ve passed an empty dependency array ([]), the function will be created only once, when the component first renders.
  • fetchData in useEffect: The memoized fetchData is passed to useEffect, and fetchData is added to the dependency array. This ensures that useEffect is called whenever the function changes, but since it’s only created once, this prevents unnecessary re-fetching.

Why Use useCallback Here?

  • Avoid unnecessary re-creations of the function: Without useCallback, the fetchData function would be recreated on every render, which could trigger unnecessary re-fetching.
  • Prevent ESLint warnings: In Create React App, if a function is defined inside useEffect and is not included in the dependency array, you’ll get an ESLint warning. Using useCallback fixes this issue and ensures the function is stable across renders.
  • Better performance: Memoizing the function ensures that it’s only recreated when necessary, improving the performance of your app.

Conclusion

By using the useCallback hook, we ensure that functions inside useEffect are only recreated when necessary. This not only prevents unnecessary re-renders and re-fetching but also helps avoid warnings in Create React App projects. This makes your app more efficient and prevents unnecessary computations.