Boosting React Performance: Minimizing Component Re-renders for Faster Rendering

Boosting React Performance: Minimizing Component Re-renders for Faster Rendering

useCallback is a React Hook that lets you cache a function definition between re-renders - React Docs.

In React applications, it is a standard procedure that whenever the state changes, the corresponding component re-renders to update its state and reflect the changes on the user interface (UI). However, if the component is a parent component that contains other components and also provides data to them, the re-rendering process can cause the child components to recursively re-render as well. This recursive re-rendering can have a significant impact on performance, particularly when dealing with components that display large amounts of data. As a result, this can often hinder the overall application performance.

useCallback, when used appropriately can help to optimize performance on issues relating to component re-rendering.

useCallback(fn, dependencies)

the useCallback, accept two parameters, fn, and dependencies.

Parameters

  • fn: The function value that you want to cache. It can take any arguments and return any values.

  • There are a few things to note about this function:

  • The function is returned and not called during the initial render. On the next renders, React will give you the same function again if the dependencies have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later.

  • The function is returned to you so you can decide when and whether to call it.

  • dependencies: The list of all reactive values referenced inside of the fn code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3].

    useCallback caches a function between re-renders until its dependencies change.

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  // Tell React to cache your function between re-renders...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...so as long as these dependencies don't change...

  // Every time the theme changes, this will be a different function...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }

  return (
    <div className={theme}>
      {/* ...ShippingForm will receive the same props and can skip re-rendering */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}


import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

By wrapping handleSubmit in useCallback, you ensure that it’s the same function between the re-renders (until dependencies change). You don’t have to wrap a function in useCallback unless you do it for some specific reason. In this example, the reason is that you pass it to a component wrapped in memo, and this lets it skip re-rendering.

In JavaScript, a function () {} or () => {} always creates a different function, similar to how the {} object literal always creates a new object. Normally, this wouldn’t be a problem, but it means that ShippingForm props will never be the same, and your memo optimization won’t work. This is where useCallback comes in handy - React Docs.

When using memoization (React.memo) in React to cache a component like ShippingForm, it helps skip re-rendering when the props remain the same. However, if a prop like handleSubmit consistently changes on each render without using useCallback, the ShippingForm component will be forced to re-render every time.

In this scenario, useCallback becomes useful. By wrapping the handleSubmit function with useCallback, you can ensure that the function is not recreated on each render unless the reactive values in its dependencies change. This prevents unnecessary re-renders of the ShippingForm component when handleSubmit itself hasn't changed.

Here's an example of how to use useCallback with ShippingForm:

import React, { useCallback } from 'react';


function ProductPage({ productId, referrer }) {
  const product = useData('/product/' + productId);

  const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
    </div>
  );
}

function post(url, data) {
  // Imagine this sends a request...
  console.log('POST /' + url);
  console.log(data);
}
import { useState } from 'react';
import ProductPage from './ProductPage.js';

export default function App() {
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <ProductPage
        referrerId="wizard_of_oz"
        productId={123}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

toggling the checkbox and changing the theme state from false to true will trigger a re-render of the App component. If the child components are not memoized with React.memo and useCallback, it will result in a recursive re-rendering of all the child components.

In scenarios where the App component has a large number of child components, this recursive re-rendering can indeed lead to delays and impact performance, especially in live applications where real-time updates are crucial.

import ShippingForm from './ShippingForm.js';

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  // Tell React to cache your function between re-renders...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...so as long as these dependencies don't change...

  // Every time the theme changes, this will be a different function...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }

  return (
    <div className={theme}>
      {/* ...ShippingForm will receive the same props and can skip re-rendering */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}



function post(url, data) {
  // Imagine this sends a request...
  console.log('POST /' + url);
  console.log(data);
}

In the scenario where the handleSubmit does not useCallback, the handlesubmit function changes whenever the theme state changes, the ShippingForm component will re-render. To avoid unnecessary re-renders of the ShippingForm component in this situation, you can use useCallback to memoize the handleSubmit function and React.memo to memoize the ShippingForm component itself.

By memoizing the handleSubmit function with useCallback and passing its dependencies, you ensure that the function is only recreated when those dependencies change. This helps prevent unnecessary re-renders of the ShippingForm component if the theme state changes without affecting the dependencies of handleSubmit.

Using useCallback in combination with React.memo can help optimize the performance of your React components by preventing unnecessary re-renders when props remain the same.

  • Caveats
  • useCallback is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.

  •   function ReportList({ items }) {
        return (
          <article>
            {items.map(item => {
              // 🔴 You can't call useCallback in a loop like this:
              const handleClick = useCallback(() => {
                sendReport(item)
              }, [item]);
    
              return (
                <figure key={item.id}>
                  <Chart onClick={handleClick} />
                </figure>
              );
            })}
          </article>
        );
      }
    

Instead, extract a component for an individual item, and put useCallback there:

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ Call useCallback at the top level:
  const handleClick = useCallback(() => {
    sendReport(item)
  }, [item]);

  return (
    <figure>
      <Chart onClick={handleClick} />
    </figure>
  );
}

Absolutely! When dealing with large amounts of data in a React application, fetching and displaying that data efficiently is crucial for optimal performance.

In such scenarios, useCallback can indeed be a valuable tool for caching functions and preventing unnecessary re-renders. By memoizing functions with useCallback, you can ensure that the function instances are cached and reused, reducing the overhead of recreating functions on each render.

This is particularly beneficial when working with components that rely on callbacks or event handlers, as it helps prevent unnecessary re-renders of components that use these functions as props. By memoizing the functions, you ensure that the components only re-render when their dependencies change.

Additionally, useCallback plays well with other optimization techniques like memoization (e.g., using React.memo) and selective rendering (e.g., using pagination or virtualization) to further enhance the performance of data-intensive applications.

By combining these optimization strategies and leveraging the benefits of useCallback, you can significantly improve the speed and efficiency of fetching and displaying large amounts of data in your React application.