React useCallback Hook Complete Guide

understanding usecallback with example code and case study


Memoization is an essential feature in web development as it can increase the performance of our website. React also knows it, that's why React gives us different features to use for optimization which is catching the result in memory and preventing unnecessary load on the application.

In these memoize techniques I'm sure you have seen useCallback hook in many projects. But do you really know the basic idea behind it or should you use it more often? I will explain it deeply so you'll understand it better.

Case Study

You can define useCallback as a function memoization technique but this definition alone can't explain anything. Let's explore it.

For this tutorial, I prepared a very basic demo application. It's a basic search functionality and also a button to change the theme color of the list of books.

import { useState} from "react";

import { getRandomColor } from "./utils";
import SearchBook from "./SearchBook";

const Famous_Books = [
  'Romeo and Juliet',
  'Pride and Prejudice',
  'Anandmath',
  'Hard Times',
  'Audacity of Hope',
  'Treasure Island',
  'Robinson Crusoe'
];

const BookApp = () => {
  const [books, setBooks] = useState(Famous_Books);
  const [themeColor, setThemeColor] = useState('#333333')

  function handleSearch(searchTerm) {
    const searchBooks = books.filter(book => book.toLowerCase().includes(searchTerm.toLowerCase()));
    setBooks(searchBooks);
  }

  function handleThemeColor() {
    setThemeColor(getRandomColor())
  }

  return (
    <div>
      <h1>Famous Books</h1>
      <div>
        <SearchBook onSearch={handleSearch} />
        <button type="button" onClick={handleThemeColor}>
          Change Theme
        </button>
      </div>
      <ul>
        {books.map((book) => <li style={{ color: themeColor}} key={book}>{book}</li>)}
      </ul>
    </div>
  );
};

export default BookApp;

useCallback example  -- search filter


  • I registered two states: one with the default theme color and the other with the Famous_books array as the initial value, which contains a list of book titles.
  • I declared event handler functions so items should render based on events.
  • The handleSearch function filters books based on text input and should only keep matching items and drop other items in the array. The handleThemeColor function changes the theme colour of a list of books.

In JSX I return SearchBook component, the code is as follows:

const SearchBook = ({ onSearch }) => {
  console.log('re-render child component')
  return (
    <input
      type="text"
      placeholder="Enter Book name"
      onChange={(event) => onSearch(event.target.value)}
        />
  )
}

The SearchBook component receives onSearch properties from the parent component and passes the input value to the parent component's handleSearch function.

Now when you search or click on the theme change button you'll see the child component being re-render. Though it's not a huge problem to be re-render because it's a very basic app and contains very little code, still we can optimize it. 

hwo to prevent child component to re-render by using usecallback?


To avoid redundant rendering of the child component when its parent changes, we could wrap the SearchBook component with the memo(). Basically, it returns memoized functional components.

import { memo } from 'react'

const SearchBook = memo(({ onSearch }) => {
  console.log('re-render child component')
  return (
    <input
      type="text"
      placeholder="Enter Book name"
      onChange={(event) => onSearch(event.target.value)}
        />
  )
})

Now after that when you click on the theme button, in Dev Tool you'll notice this strange behavior; it still re-renders the child component, SearchBook.

So what happened here? Is there a problem with the memo function? Let's again look at the memo function definition.

The Memo function compares the previous props value to the current props value and only executes the memoize component again if the props are not the same as the prior render.

Let's look at the props. The searchBook component receives the onSearch prop, which accepts a function as a value. 

const SearchBook = ({ onSearch }) => { }

According to the memo function definition, the onSearch property changes every time the parent component is re-rendered. But why is such behavior occurring?

When to use useCallback?

When a component function runs or re-renders, all of the code inside that component is likewise run, causing the state to registered again, the function to be regenerated, and the JSX code to run once more.

In Javascript, when you compare strings, it returns true.

 "react" === "react" // true 

When you compare numbers, it returns true.

 30 === 30 // true 

But when you compared functions will return false

 () === () // false

The comparison of Objects will also return false because two objects created with the same code are not equivalent.

{} === {} // false 

In JavaScript, functions are just values, or as we can say, objects. So in React when component function is re-render a new function object is re-created.

As a result, when the BookApp component re-renders, a new handleSearch function is created, which is why the function prop is not recognized as equal, and the onSearch prop in the search book component varies between render cycles.

When React compares this new render value to the previous render props value, it notices that they are not the same, which is why memo function optimization will not work here; it will re-execute the child component even if the props remain unchanged.

useCallback

For this complex problem, React gives us a simple solution, We can stop re-execution of the function by using useCallback hook.

useCallback(callback, dependency)

The useCallback hook returns the memoize function and runs it only if one of the dependencies changesIt is especially useful when a function is passed down to a child component as a prop to maximize productivity.

The useCallback accept function that we want to memoize as a first argument and an external variable `books` which this function depends on, a dependency, as a second argument.

useCallback returns the function that you wrapped; you can give the same name as your function name.

const handleSearch = useCallback(function handleSearch(searchTerm) {
    const searchBooks = books.filter(book => book.toLowerCase().includes(searchTerm.toLowerCase()))

    setBooks(searchBooks)
  }, [books])

An empty dependency array may be used in cases where the function depends on no external variables.

By wrapping function with useCallback we prevent the function object from being re-created each time its parent component executes, allowing to use of the same function between renders.

Now that useCallback has memoized the handleSearch function, the SearchBook component will no longer render.

Optimizing callback in useEffect

In other cases, we can optimize callback in Effect. Let's say I need to build a network connection that has side effects, so I'll utilize useEffect.

function fetchData() {
    // connect...
  }

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

I've provided the function fetchData as a dependency array. As we all know, if any code inside of Effect depends on an external source, it must be included as a dependency array. The issue arises when we pass a function as a dependency since it can cause us to become trapped in an endless loop.

When you add a dependency array, you notify React that Effect method should execute whenever the dependency value changes.

As we know when a component function is re-executed, it creates a new function object and React uses it as a new value. When a prop function receives a new value, React compares the two values to determine that these two are different.

It will cause your Effect to call the function repeatedly, and it will cause your application to break.

To fix this, wrap the function you need to call from an Effect into useCallback:

const fetchData = useCallback(function fetchData() {
    // connect...
  }, [])

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

useCallback Best Practice

It's easy to use and fixes performance issues, but should you use the useCallback hook for every event handler? The answer is a big NO!

You should only utilize this hook for performance reasons, such as avoiding unnecessary rendering and optimizing callbacks in useEffect. Be careful not to misuse it.

Conclusion

The useCallback store function itself and returns memoize functions. in this article, I demonstrated search functionality so you can understand function optimization better and the idea behind using it.

it's a very important feature of React but don't overdo it because it won't benefit you in any way, but will instead harm web performance and cause problems. 

I also write an article about useMemo hook to improve app performance. This gives you the same solution but for a different problem. 

0 Comments:

Post a Comment