Computer scienceFrontendReactHooks

useCallback

9 minutes read

useCallback is a hook React provides that can help you enhance performance in your React application, particularly in components with a complex render hierarchy. It helps you avoid unnecessary re-renders and computations by remembering a function and updating it only when its dependencies change. In this topic, you'll learn about how to apply this hook and see common examples of its use.

How to use it

Normally, in React, if you define a function inside a component, it gets recreated every time the component re-renders. This can cause unnecessary re-renders if the function is given as a prop to a child component; the child will treat it as a new prop and re-render itself too.

useCallback solves this by returning a memoized version of the function that only changes if one of the dependencies changes. So, if you pass the function to a child component, it won't lead to unnecessary re-renders because the function reference will stay the same between renders unless a dependency changes.

Here's the basic syntax of useCallback:

const memoizedCallback = useCallback(callback, dependencies);

The callback argument is the function you want to memoize, and the dependencies argument is an array of values that the memoized function depends on. If any of the dependencies change, the memoized function will be re-created; otherwise, it will return the same memoized function instance.

Here's an example to show how it works:

import React, { useState, useCallback, memo } from 'react';

function ProfilePage({ userId }) {

  // Tell React to keep your function the same between re-renders...
  const handleSubmit = useCallback((userInfo) => {
    post('/user/' + userId + '/update', {
      userInfo,
    });
  }, [userId]);

  return (
    <div>
      {/* UpdateForm will get the same props and can skip re-rendering */}
      <UpdateForm onSubmit={handleSubmit} />
    </div>
  );
}

const UpdateForm = memo(function UpdateForm({ onSubmit }) {
  console.log('UpdateForm rendered');
  // ...
});

Here, we see a drop in performance when rendering the UpdateForm, so useCallback is used to memoize the handleSubmit function. This function relies on the userId prop. The useCallback hook will give back the same function every time the component re-renders, as long as userId stays the same. If userId changes, useCallback will give back a new function.

The handleSubmit function is then given as a prop to the UpdateForm component. UpdateForm is a memoized component, meaning it only re-renders if its props change. Since handleSubmit is memoized with useCallback, UpdateForm won't re-render needlessly when the parent ProfilePage component re-renders, unless userId changes. You can check this by logging a message in UpdateForm.

The React.memo function is a higher order component that optimizes functional components by wrapping them and preventing re-renders if the props don't change. It does a shallow comparison of the old and new props and re-renders the component only if there's a change.

You can also use the useCallback hook with an empty dependency array. When you have an empty dependency array ([]) with useCallback, it means the memoized function will be created just once and will not rely on any values.

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

function Input() {
  const [text, setText] = useState('');

  const handleChange = useCallback((event) => {
    setText(event.target.value);
  }, []);

  return (
    <div>
     <input type="text" value={text} onChange={handleChange} />;
     <p>{text}</p>
    </div> 
 );
}

The handleChange function is memoized using useCallback. Even though it has no dependencies, memoizing it is still useful to stop unnecessary re-renders of the Input component. Every time you type in the input field, the handleChange function is called with the new value, and the Input component re-renders to show this new value.

When to use it?

Should you use useCallback all the time? No, you don't need to use useCallback all the time. It's a tool for making your app run faster, and if you use it when you don't need to, it can make your code more complicated than it needs to be.

Here's an example where useCallback might not be necessary:

import React, { useState } from 'react';

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

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Click me</button>
    </div>
  );
}

In this case, even though the increment function is created on every render, it's not a problem because it's not being passed to a child component that needs to stay the same to avoid re-renders. The function is just for the onClick handler of a button, so making it again each time you render isn't a big deal.

Here are some guidelines to help you decide whether to use useCallback:

  • Use it for functions that take a lot of time or are used a lot: If you have a function that does a lot of work or is called often, useCallback can help. It saves the function so it doesn't have to be created again, which can make things faster.

  • Use it when giving callback functions to child components: When you give callback functions to child components, useCallback can stop those children from re-rendering for no reason by saving the callbacks.

  • Think about how it affects how easy your code is to read and keep up with: useCallback can add complexity, particularly with dependencies. You should think about whether the faster performance is worth this extra complexity and if it makes your codebase easier or harder to understand and maintain.

  • Think about your component's size and how complex it is: If your component isn't big and doesn't have many dependencies or callbacks, using useCallback might not change much. In such cases, you might want to keep the code easy to read and work with.

Let's look at an example of making a child component render more efficiently:

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

function ParentComponent() {
  const [name, setName] = useState('');

  const handleChange = useCallback((event) => {
    setName(event.target.value);
  }, []);

  return (
    <div>
      <input type="text" value={name} onChange={handleChange} />
      <ChildComponent onNameChange={handleChange} />
    </div>
  );
}

The ParentComponent has an input field and a ChildComponent. The handleChange function updates the name state in the ParentComponent and also gets passed as a prop to the ChildComponent.

By using useCallback, the handleChange function is saved and will only be made again if its dependencies change. In this example, the function doesn't have any dependencies, so it uses an empty dependency array [].

The ChildComponent uses the onNameChange prop as a callback for handling name changes. Using useCallback makes sure the ChildComponent doesn't re-render for no reason when the ParentComponent re-renders, even if other state or props change.

Using useCallback in this case can make your app run more smoothly by avoiding needless re-renders of the ChildComponent when the ParentComponent renders and only making the callback function again when it needs to change.

Conclusion

By using useCallback, you can make sure a function is only made again when its dependencies change. This stops components that depend on these functions from re-rendering for no reason, leading to better performance and a smoother rendering process.

However, it's important to use useCallback wisely. Using it on all functions without thinking it through can make your code more complicated than it needs to be. It works best for functions that take a lot of computing power, are used often, or when you give callbacks to child components that depend on them.

4 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo