React Performance: useCallback vs. useMemo Hooks

React Performance: useCallback vs. useMemo Hooks

·

5 min read

React, the renowned JavaScript framework for front-end development, has revolutionized the way developers build user interfaces through its innovative approach to component-driven development.

With the advent of React 16, we witnessed a transformative shift from class-based components to functional components. Alongside this shift, React introduced game-changing hooks that have become the backbone of modern React development:

  • useEffect, your trusty companion for managing a component's lifecycle.

  • useState, the go-to solution for state management.

These hooks aren't just fancy utilities; they're essential for optimizing your application's performance. In a world where minimizing re-renders and re-computations is paramount, these hooks are your secret weapons.

But the React journey doesn't stop there. Recently, two more formidable hooks have entered the scene: useCallback and useMemo. These aren't just newcomers; they're powerhouses. They've brought with them the promise of further enhancing your application's performance by eliminating unnecessary re-renders and computations.

In this blog, we're going to dive deep into these hooks. We'll unravel their mysteries, understand how they function, and most importantly, discover when and where to deploy them in your projects. Through practical examples and real-world scenarios, we'll equip you with the knowledge to wield useCallback and useMemo effectively.

So, if you're ready to supercharge your React applications and make them lightning-fast, let's embark on this journey together!

useCallback


const memoizationFunction = useCallback(()=>{
  // doSomething;
}, [dependencies]);

For example, we will use the debounce function on the input field to get the data only when the user stops writing for a specific amount of time.

import React, { useState } from "react";
const App = () => {
  const [text, setText] = useState("");
  const debounce = (func, delay) => {
    let inDebounce;
    return function () {
      const context = this;
      const args = arguments;
      clearTimeout(inDebounce);
      inDebounce = setTimeout(() => func.apply(context, args), delay);
    };
  };
  const makeApiCall = () => {
    console.log(text, " Making an API call");
  };
  const debouncedApiCall = debounce(makeApiCall, 500);
  const onChangeHandler = (e) => {
    setText(e.target.value);
    debouncedApiCall();
  };
  return (
    <div>
      <input type="text" onChange={onChangeHandler} value={text} />
    </div>
  );
};

Every time the state is updated, the component re-renders, and the debounce function is reattached to the debouncedApiCall variable. As a result, the logs are printed every time the state changes, and the debounce needs to be fixed.

To avoid this, we can wrap the debounce function inside useCallback, and the program will write the memoized debounce function that will change only if dependencies change.

import React, { useState, useCallback } from "react";
const App = () => {
  const [text, setText] = useState("");
  const debounce = (func, delay) => {
    let inDebounce;
    return function () {
       const context = this;
       const args = arguments;
       clearTimeout(inDebounce);
       inDebounce = setTimeout(() => func.apply(context, args), delay);
    };
  };
  const makeApiCall = (e) => {
    console.log(e, "Making an API call");
  };
  const debounce = useCallback(()=> debounce(makeApiCall, 2000), []);

  const onChangeHandler = (e) => {
    setText(e.target.value);
    debounce(e.target.value);
  };
  return (
    <div>
      <input type="text" onChange={onChangeHandler} value={text} />
    </div>
  );
};

We don’t want to re-render the _debounce function, so we are not passing any dependencies. This code will print the log only when the user has stopped writing and 2000 ms have passed. Notice in the code that we are passing the value to debounce(e.target.value) because it cannot pull the value of the state.

Even though it seems to be working fine, it is not. If you have ESLint enabled, you will notice that we are getting the following linting error for the useCallback hook:

error React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead react-hooks/exhaustive-deps

This is because the dependencies of the function that useCallback is taking as input are not known, so ESLint wants us to write an inline function.

Also, the debounce function is re-created on every re-render, thus changing its reference and defeating the purpose of caching it.

To fix this, we can create a custom useDebounce hook and use it.

import { useState, useCallback, useRef } from "react";
const useDebounce = (func, delay) => {
  const inDebounce = useRef();
  const debounce = useCallback(
    function () {
      const context = this;
      const args = arguments;
      clearTimeout(inDebounce.current);
      inDebounce.current = setTimeout(() => func.apply(context, args), delay);
    },
    [func, delay]
  );
  return debounce;
};
const App = () => {
  const [text, setText] = useState("");
  const makeApiCall = useCallback((e) => {
    console.log(e, "Making an API call");
  }, []);
  const debounce = useDebounce(makeApiCall, 2000);
  const onChangeHandler = (e) => {
    setText(e.target.value);
    debounce(e.target.value);
  };
  return (
    <div>
      <input type="text" onChange={onChangeHandler} value={text} />
    </div>
  );
};

useMemo hook

Syntax

const computedValue = useMemo(()=>{
  //return value
}, [dependencies]);

useMemo works similarly to the useCallback, but rather than returning the function, it returns the computed value from the function, i.e. the function’s output, and only recomputes the function when the dependencies change to provide the new result.

For example, let’s say we have a child component that should render only when the text has a specific value.

import React, { useState, useCallback } from "react"
const ChildComponent = ({ text }) => {
  return <p>{text}</p>;
};
const App = () => {
  const [text, setText] = useState("");
  const onChangeHandler = (e) => {
    setText(e.target.value);
  };
  return (
    <div>
      <input type="text" onChange={onChangeHandler} value={text} />
      <ChildComponent text={text} />
    </div>
  );
};

If the conditional check were placed within the child component, it would trigger a re-render each time the parent’s state updates, regardless of our intention to limit it to certain values.

The useMemo hook addresses this. By employing this hook, the child component becomes memoized, ensuring it only re-renders when the conditions specified in its dependencies are satisfied.

import React, { useState, useMemo } from "react";
const ChildComponent = ({ text }) => {
  return <p>{text}</p>;
};
const App = () => {
  const [text, setText] = useState("");
  const onChangeHandler = (e) => {
    setText(e.target.value);
  };

  const memoizedChildComponent = useMemo(() => <ChildComponent text={text} />, [text === 'myblog'])
  return (
    <div>
      <input type="text" onChange={onChangeHandler} value={text} />
      {memoizedChildComponent}
    </div>
  );
};

Conclusion

useCallback and useMemo are tools to optimize your React components, and the choice between them depends on whether you need to memoize functions (use useCallback) or values (use useMemo). Using them correctly can help improve the performance of your React applications by avoiding unnecessary re-renders and calculations.

Did you find this article valuable?

Support sivalaxman by becoming a sponsor. Any amount is appreciated!