Optimizing React Components

14. April, 2023 5 min read Teach

Using the useCallback Hook

One of the core concepts of React is a component, a modular unit that encapsulates a piece of UI functionality.

Components can have a state, which is used to manage data and trigger updates to the UI. In some cases, however, a component’s state changes can cause unnecessary re-renders, leading to performance issues.

Memoizing a Function

useCallback is a React hook that memoizes a function, meaning it stores a cached version, and returns it whenever the hook is called again with the same inputs. This approach can improve performance by preventing unnecessary re-renders in some instances.

Let’s have a look at an example:

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

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

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

Above, we have a component that displays a count and a button to increment it. We define a handleClick function that updates the count when clicking the button. Typically, whenever the component re-renders, a new instance of handleClick is created, even if the count hasn’t changed. This process can cause unnecessary re-renders and hurt performance. By wrapping handleClick in useCallback, we can memoize it so that it only changes when the count changes.

The second argument to useCallback is an array of dependencies. These are the values that the function relies on, and they determine when the function should be memoized again. In this case, we only want to memoize the function when the count changes, so we include [count] as the dependency.

Deep dive

Let’s create a more comprehensive example. We want to use a Parent component that renders three instances of a Child component. Each Child component takes a name and an onClick prop that is triggered when pressing the button:

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

const Child = ({ name, onClick }) => {
  const handleClick = useCallback(() => {
    onClick(name);
  }, [name, onClick]);

  return <button onClick={handleClick}>{name}</button>;
};

const Parent = () => {
  const [selectedName, setSelectedName] = useState('');

  const handleChildClick = useCallback(name => {
    setSelectedName(name);
  }, []);

  return (
    <div>
      <p>Selected name: {selectedName}</p>
      <Child name="Thor" onClick={handleChildClick} />
      <Child name="Loki" onClick={handleChildClick} />
      <Child name="Odin" onClick={handleChildClick} />
    </div>
  );
};

The handleChildClick function in Parent sets the selectedName state to the clicked name. Usually, every time the Parent component re-renders, a new instance of handleChildClick is created, even though it doesn’t rely on any props or state. By wrapping it in a useCallback, we can memoize the function so that it only changes when its dependencies change.

The handleClick function in Child also uses a useCallback to memoize the function passed to the onClick prop. This approach ensures that a new function is only created when the name prop or onClick prop changes. This result can be particularly useful if the Child component is expensive to render and should only re-render when its props change.

When to avoid it

While useCallback can be a powerful tool for optimizing performance, there are some cases where it should not be used. Here are a few scenarios where it might be best not to use it:

When the function is cheap to compute

If the function you’re using is relatively cheap to compute, it may not be worth the overhead of using useCallback. In these cases, you may be better off creating a new function each time the component re-renders.

For example, let’s say you have a simple button component that logs a message to the console when it’s clicked:

import React from 'react';

const Button = () => {
  const handleClick = () => {
    console.log('Button clicked!');
  };

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

When the function doesn’t cause unnecessary re-renders

If the function you’re using doesn’t cause unnecessary re-renders, there’s no need to memoize it. This is particularly true for functions that are only called once and don’t rely on any props or state.

For example, let’s say you have a simple component that displays a message:

import React from 'react';

const Message = () => {
  const message = 'Hello, world!';

  return <div>{message}</div>;
};

When the function has no dependencies

If your function doesn’t rely on any props or state, there’s no need to memoize it either. This is because the function won’t change between re-renders.

For example, let’s say you have a simple component that displays the current date:

import React from 'react';

const DateDisplay = () => {
  const currentDate = new Date();

  return <div>{currentDate.toString()}</div>;
};

Summary

The useCallback hook in React can improve the performance of components by memoizing a function to prevent unnecessary re-renders. Especially when the function is expensive to compute, or a component needs to be optimized for performance. However, it should only be used judiciously when necessary, as overusing it can add complexity to the code and lead to unnecessary memory usage.

‘Till next time!