Implementing Robust Error Handling with Performance Optimization (memo, useCallback, useMemo) in React

Sabin ChapagainSabin Chapagain
3 min read

As React developers, implementing robust error handling is crucial for a seamless user experience. However, excessive re-rendering of components can significantly affect the performance of a React application, especially when dealing with complex state updates. In this post, we'll explore common misconceptions about performance optimization in React using memo, useCallback, and useMemo and provide practical examples and corrected code snippets.

Misconception 1: Overusing useCallback for State Updates

Example code:

import React, { useState } from 'react';

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

  const seldomComponent = React.useCallback((e) => {
    console.log(`Received props: ${e}`);
  }, [count]);

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

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

In this example, every component re-render occurs, even though seldomComponent doesn't need to change. Fixing this, we use useCallback's dependency array to only create a new seldomComponent function if and only if the count state changes.

Corrected Code:

import React, { useState } from 'react';

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

  const seldomComponent = React.useCallback((e) => {
    console.log(`Received props: ${e}`);
  }, []);

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

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

Misconception 2: Misusing useMemo

Example code:

import React from 'react';

function MyComponent() {
  const result = useMemo(
    () => {
      let isRecipeLoaded = false;
      const promises = [
        new Promise((resolve, reject) => {
          // awaiting for some api
          setTimeout(() => {
            resolve();
          }, 1000);
        }),
        new Promise((resolve, reject) => {
          setTimeout(() => {
            isRecipeLoaded = true;
            resolve();
          }, 2000);
        }),
      ];
      return result;
    },
    [] // WRONG! HERE
  );

  return (
    <div>
      {isRecipeLoaded && <button></button>}
    </div>
  );
}

Here, isRecipeLoaded is not properly memoized. The array passed to useMemo is empty, meaning it will only run once as soon as the component is initialized. Let's fix this.

Corrected Code:

import React from 'react';

function MyComponent() {
  const isRecipeLoaded = React.useMemo(() => {
    let result = false;
    const promises = [
      new Promise((resolve, reject) => {
        // awaiting for some api
        setTimeout(() => {
          result = true;
          resolve();
        }, 1000);
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          result = true;
        }, 2000);
      }),
    ];
    result;
  }, []); // CORRECT!

  return (
    <div>
      {isRecipeLoaded && <button></button>}
    </div>
  );
}

In the corrected version, useMemo now properly depends only on the ref value it needs, namely to indicate if it has loaded. The memoization will be then triggered whenever useMemo runs and not more often (avoiding performance degradation).

Misconception 3: Overusing memo for Trivial Components

Here's a minor code snippet where the initial render of the Avatar doesn't utilize the computed idDataAvatars.userInfo.currentAvatarPorts unless you update within the latter group.

function Avatar(props) {
  const idDataAvatars = useMemo(() => {
    // complicated computation to extract avatar via filtering
  }, [props.user.userInfo.currentAvatarPorts]);

  const [likes, setLikes] = useState(() => 1);

  return (
    <div>
      <img src={idDataAvatars}></img>
      <button Liked by {likes} users, like this! onClick={() => setLikes(likes + 1)}>Like</button>
    </div>
  );
}

In the initial render, it is unnecessary to do anything even more computational, namely we simply use the state to update, yet unnecessarily caluclate idDataAvatars.

Here's the corrected code.

Corrected Code:

import React from 'react';

function Avatar(props) {
  const idDataAvatars = React.useMemo(() => {
    // complicated computation to extract avatar via filtering
  }, [props.userInfo.currentAvatarPorts]);

  const [likes, setLikes] = useState(() => 1);

  return (
    <div>
      <img src={idDataAvatars}></img>
      <button Liked by {likes} users, like this! onClick={() => setLikes(likes + 1)}>Like</button>
    </div>
  );
}

In this corrected code, we memoize idDataAvatars via the computed props.userInfo.currentAvatarPorts, and delegate the state change to likes respectively. This fundamental combination exploits the React lifecycle to ensure both sides' one-offs are triggered once.

0
Subscribe to my newsletter

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

Written by

Sabin Chapagain
Sabin Chapagain