Boosting React Performance: A Deep Dive into "useCallback" and "React.memo"

AsawerAsawer
4 min read

useCallback and React.memo Both are powerful tools in React to optimize rendering performance, especially in complex applications. When used effectively, they help avoid unnecessary re-renders, making your app more efficient.


1. useCallback Hook

The useCallback hook memoizes a function, preventing it from being recreated on every render. This is useful when passing functions down to child components as props, because without useCallback, the child would re-render every time the parent component re-renders, even if the function itself hasn’t changed.

Syntax

const memoizedFunction = useCallback(() => {
  // Your function logic here
}, [dependencies]);

Parameters:

  • Function to memoize: This is the function you want React to keep from recreating unless dependencies change.

  • Dependency array: Similar to useEffect, it specifies when the function should be updated. If any dependency changes, React recreates the function.

Example

Imagine a component with a button that updates a counter and a function that handles logging. We'll see how useCallback optimizes it.

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

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

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

  // Memoizing the logging function with useCallback
  const logCount = useCallback(() => {
    console.log(`Current count: ${count}`);
  }, [count]); // Only re-creates the function if `count` changes

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>Increment</button>
      <button onClick={logCount}>Log Count</button>
    </div>
  );
}

Explanation:

  • Without useCallback: Every time Counter re-renders (like when increment is called), logCount would be recreated, causing any child components that depend on it to re-render as well.

  • With useCallback: logCount only updates when count changes, ensuring that child components relying on logCount don’t re-render unnecessarily.


2. React.memo

React.memo is a higher-order component that memoizes the component itself. It prevents the component from re-rendering unless its props change, which is ideal for functional components receiving the same props repeatedly.

Syntax

const MemoizedComponent = React.memo(Component);

Example:

Now, let’s add a child component that displays the count and only re-renders when the count itself changes.

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

// Memoized child component
const DisplayCount = React.memo(({ count }) => {
  console.log('DisplayCount rendered');
  return <h2>Count: {count}</h2>;
});

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

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

  const logCount = useCallback(() => {
    console.log(`Current count: ${count}`);
  }, [count]);

  return (
    <div>
      <DisplayCount count={count} />
      <button onClick={increment}>Increment</button>
      <button onClick={logCount}>Log Count</button>
    </div>
  );
}

Explanation:

  • Without React.memo: Every time Counter re-renders, DisplayCount would re-render as well, even if count hasn’t changed.

  • With React.memo: DisplayCount only re-renders if its prop (count) changes, making it more efficient.

Key Takeaways and Best Practices

  • Use useCallback when passing functions down to child components to avoid function recreation and unnecessary re-renders.

  • Use React.memo to wrap components that receive stable props, ensuring they only re-render when their props change.

Combining useCallback with React.memo for Optimal Performance

Let’s create an example where these two are combined effectively:

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

const MemoizedButton = React.memo(({ handleClick, label }) => {
  console.log(`Rendering button - ${label}`);
  return <button onClick={handleClick}>{label}</button>;
});

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

  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  const toggleOtherState = useCallback(() => {
    setOtherState(prevState => !prevState);
  }, []);

  return (
    <div>
      <h2>Count: {count}</h2>
      <MemoizedButton handleClick={increment} label="Increment Count" />
      <MemoizedButton handleClick={toggleOtherState} label="Toggle State" />
      <p>Other state: {otherState ? 'On' : 'Off'}</p>
    </div>
  );
}

Breakdown:

  • MemoizedButton: A memoized component that receives handleClick and label as props. It only re-renders when these props change.

  • increment and toggleOtherState: Both functions are memoized using useCallback, preventing unnecessary re-renders of MemoizedButton.

Benefits:

  • Reusability: The memoized component MemoizedButton is only updated when necessary, optimizing rendering time.

  • Code Readability: useCallback and React.memo combined help make the intent of the code clearer by explicitly defining when functions and components should or shouldn’t re-render.

By combining useCallback and React.memo, you can control re-renders on a granular level, ensuring your app remains performant and readable. This approach is particularly useful for apps with complex component trees or when you need to pass callbacks deeply into the tree.

0
Subscribe to my newsletter

Read articles from Asawer directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Asawer
Asawer