React useCallback

React useCallback

Dead Simple Chat Team

Table of Contents

In this blog post, we will see how to use React useCallback hook.

What is its purpose we will also look at real-world scenarios where it should be used and common pitfalls to avoid when using React useCallback.

This article is brought to you by DeadSimpleChat: Chat API and SDK provider

What is useCallback?

useCallback is used to cache the function "definition". It is typically used in conjunction with React memo.

If you have cached your component with React memo so that it doesn't re-render unless its props are changed.  

If you are passing the component a function, then your function will re-render every time, defeating the purpose of using memo.

Because in Javascript function() {} or () => { } create a different function, thus the prop will never be the same, causing the component to re-render, make memo useless.

To prevent this from happening, you can wrap the function definition inside useCallback and this prevents the re-creation of the function unless the dependencies changes.

Syntax of useCallback

The useCallback hook accepts two parameters, one is the method/function you would like to cache, and the second parameter is the dependency array, which returns the cached function.

When the variable passed in the dependency array changes, the useCallback hook returns and updated function.

const method = useCallback(<METHOD>, [<DEPENDENCY_ARRAY]);

Let's look at some examples to understand it better.

Dead Simple Chat allows you to integrate chat in minutes into your React or web application using the powerful Javascript Chat SDK.

Example of useCallback

Consider the following code:

import React, { useState } from "react";

const ChildComponent = React.memo(({ onButtonClick }) => {
  console.log("ChildComponent rendered");
  return <button onClick={onButtonClick}>Increment</button>;
});

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

  const [theme, setTheme] = useState("light");

  const handleButtonClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>Current Theme: {theme}</h1>
      <button
        onClick={() => {
          theme === "light" ? setTheme("dark") : setTheme("light");
        }}
      >
        Toggle Theme
      </button>
      <h1>Counter: {count}</h1>
      <ChildComponent onButtonClick={handleButtonClick} />
    </div>
  );
}

export default ParentComponent;

In the above code we have created a ChildComponent and cached it using React.memo.

A Brief  Primer on React Memo:

By using React.memo the component will not re-render unless the props are changed.

Typically when the parent component is re-rendered all the child components are also re-rendered.

But by using React memo on a child component, when the parent component is re-rendered the Child Component is not re-rendered unless the props of the Child Component change.

In the ChildComponent we have also added a console.log statement to print on the console "ChildComponent rendered".

We are accepting a method onButtonClick as a prop of the ChildComponent and calling the method on the onClick event when the button is pressed.

Next, we have created a ParentComponent and in the ParentComponent we have created two state variables, one is count and another one is theme.

In the ParentComponent we have created a method called as handleButtonClick which increments our state variable count and we are passing this method as a prop to the ChildComponent.

We have also created a button to toggle the second state variable called as the theme and when the button is pressed we are toggling the theme from light to dark.

We are also displaying the current theme and count.

As we have used React.memo our expected behaviour would be when we toggle the theme, we should not see, the ChildComponent rendered message on the screen.

Let's try it out:

0:00
/

As you can see in the above video, each time the "Toggle Theme" button is pressed the "ChildComponent rendered" message is printed on the screen.

Why is this happening?

We had discussed in the React useCallback intro, it happens because JavaScript creates a new function each time it the component is rendered.

And we are passing the function as a prop, and React memo will see it as a new function and re-render the child component.

To solve this problem we will wrap our function in useCallback and it will return a cached version of our function.

Here is the updated code:

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

const ChildComponent = React.memo(({ onButtonClick }) => {
  console.log("ChildComponent rendered");
  return <button onClick={onButtonClick}>Click me</button>;
});

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

  const [theme, setTheme] = useState("light");

  // Using useCallback to cache the function
  const handleButtonClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <h1>Current Theme: {theme}</h1>
      <button
        onClick={() => {
          theme === "light" ? setTheme("dark") : setTheme("light");
        }}
      >
        Toggle Theme
      </button>
      <h1>Counter: {count}</h1>
      <ChildComponent onButtonClick={handleButtonClick} />
    </div>
  );
}

export default ParentComponent;

In our updated code we have wrapped the function inside useCallback hook. Let's see how our updated code performs:

0:00
/

As you can see in the console, the "ChildComponent rendered" message is not printed each time we press the "Toggle Theme" button.

Let's look at some real-world scenarios where to use useCallback.

Real-world Scenarios of useCallback

We will look at some real-world scenarios with code examples where using useCallback would be useful.

Memoize Data Fetching Function in Infinite Scroll

In an infinite scrolling list we can use the useCallback to cache the function that is responsible to fetch the data to prevent unnecessary renders and API calls.

Let's look at an example, we will build a simple infinite scrolling listing using the freely available Github List Users API.

In this example, we will use useCallback and useEffect in conjunction to prevent unnecessary API calls.

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

function InfinitUserScroll() {
  const [users, setUsers] = useState([]);
  const [page, setPage] = useState(1);

  const fetchData = useCallback(async () => {
    const response = await fetch(
      `https://api.github.com/users?since=${page * 30}`
    );
    const nextData = await response.json();
    setUsers((curData) => [...curData, ...nextData]);
  }, [page]);

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

  const handleScroll = (e) => {
    const { scrollTop, scrollHeight, clientHeight } = e.target;
    if (scrollHeight - scrollTop === clientHeight) {
      setPage((prevPage) => prevPage + 1);
    }
  }

  return (
    <>
      <div
        onScroll={handleScroll}
        style={{ overflowY: "scroll", height: "400px" }}
      >
        <h1>Github Users</h1>
        <hr />
        {users.map((item, index) => (
          <div key={index}>{item.login}</div>
        ))}
      </div>
    </>
  );
}

export default InfinitUserScroll;

In the above code we are using useCallback to cache  methods  one is the fetchData method.

The fetchData method is cached and page state variable is the dependency passed to the useCallback caching the fetchData method.

Then we are passing fetchData as a dependency in useEffect and calling fetchData method in useEffect.

Listing page as a dependency in useCallback causes the fetchData method to be re-created only when the page value changes.

When the page value changes, new fetchData method is created which causes the useEffect to trigger to fetch the new page info.

Util the page value is updated the fetchData method is not called thus preventing unnecessary API calls.

Debouncing Input to prevent excessive calls to API

We can also use useCallback hook to debounce the user input that calls an API for example a search API.

Debouncing the user input prevent excessive calls to the API, thus preventing the server from being overloaded or if you are using a 3rd party API prevents the API costs.

Let's code at the code example:

import React, { useState, useEffect, useCallback } from "react";
import debounce from "lodash.debounce";

function WikiSearch({ searchDelay = 300 }) {
  const [searchTerm, setSearchTerm] = useState("");
  const [results, setResults] = useState([]);

  const performSearch = async (term) => {
    const response = await fetch(
      `https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srsearch=${term}`
    );
    const data = await response.json();
    setResults(data.query.search);
  };

  const debouncedSearch = useCallback(
    debounce((term) => {
      performSearch(term);
    }, searchDelay),
    [searchDelay]
  );

  useEffect(() => {
    if (searchTerm) {
      debouncedSearch(searchTerm);
    } else {
      setResults([]);
    }
  }, [searchTerm, debouncedSearch]);

  return (
    <div>
      <h3>Wiki Search Engine</h3>
      <hr />
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search Wikipedia."
      />
      {results.length > 0 ? (
        <h1>Wikipedia Search Results</h1>
      ) : (
        <div>
          <br />
          <strong>Nothing Found</strong>
        </div>
      )}
      <ul>
        {results.map((result) => (
          <li key={result.pageid}>
            <a
              href={`https://en.wikipedia.org/?curid=${result.pageid}`}
              target="_blank"
              rel="noreferrer"
            >
              {result.title}
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default WikiSearch;

In the above code example, we are using the Wikipedia API to search Wikipedia to fetch a list of pages.

We are adding a default debounce of 300ms and using the lodash.debounce library and creating a cached de-bounced method using useCallback

We have created a WikiSearch component that accepts an optional searchDelay.

Next, we have created a method called as performSearch, the method will fetch the information and set it in the results date variable.

Using the useCallback, we have created a debouncedSearch method and called the useCallback hook to debounce the search.

Then finally in the useEffect hook we are calling the debouncedSearch method.

Here is the demo:

Handling Events in a List

When you a list of items with each item has an event handler, then we can use the useCallback to cache the handler function.

To demonstrate this we will create a TodoList component, that will display a list of Todos.

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

const TodoItem = React.memo(({ item, onToggle }) => (
  <li>
    <input
      type="checkbox"
      checked={item.completed}
      onChange={() => onToggle(item.id)}
    />
    {item.name}
  </li>
));

function TodoListComponent() {
  const [todos, setTodos] = useState([
    { id: 1, name: "Todo 1", completed: false },
    { id: 2, name: "Todo 2", completed: false },
    { id: 3, name: "Todo 3", completed: false },
    { id: 4, name: "Todo 4", completed: false }
  ]);

  const handleToggle = useCallback((id) => {
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} item={todo} onToggle={handleToggle} />
      ))}
    </ul>
  );
}

export default TodoListComponent;

We have created the handleToggle method and caching it using useCallback, and passing the handleToggle to the <TodoItem /> list.

Conclusion

In this blog post, we have learned how to use useCallback and the real-world scenarios and examples.