One place for hosting & domains

      useMemo

      How To Avoid Performance Pitfalls in React with memo, useMemo, and useCallback


      The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

      Introduction

      In React applications, performance problems can come from network latency, overworked APIs, inefficient third-party libraries, and even well-structured code that works fine until it encounters an unusually large load. Identifying the root cause of performance problems can be difficult, but many of these problems originate from component re-rendering. Either the component re-renders more than expected or the component has a data-heavy operation that can cause each render to be slow. Because of this, learning how to prevent unneeded re-renders can help to optimize the performance of your React application and create a better experience for your user.

      In this tutorial, you’ll focus on optimizing performance in React components. To explore the problem, you’ll build a component to analyze a block of text. You’ll look at how different actions can trigger re-renders and how you can use Hooks and memoization to minimize expensive data calculations. By the end of this tutorial, you’ll be familiar with many performance enhancing Hooks, such as the useMemo and useCallback Hook, and the circumstances that will require them.

      Prerequisites

      • You will need a development environment running Node.js; this tutorial was tested on Node.js version 10.22.0 and npm version 6.14.6. To install this on macOS or Ubuntu 18.04, follow the steps in How to Install Node.js and Create a Local Development Environment on macOS or the Installing Using a PPA section of How To Install Node.js on Ubuntu 18.04.

      • A React development environment set up with Create React App, with the non-essential boilerplate removed. To set this up, follow Step 1 — Creating an Empty Project of the How To Manage State on React Class Components tutorial. This tutorial will use performance-tutorial as the project name.

      • If you are new to debugging in React, check out the tutorial How To Debug React Components Using React Developer Tools, and familiarize yourself with the developer tools in the browser you are using, such as Chrome DevTools and Firefox Developer Tools.

      • You will also need a basic knowledge of JavaScript, HTML, and CSS, which you can find in our How To Build a Website With HTML series, How To Build a Website With CSS series, and in How To Code in JavaScript.

      Step 1 — Preventing Re-renders with memo

      In this step, you’ll build a text analyzing component. You’ll create an input to take a block of text and a component that will calculate the frequency of letters and symbols. You’ll then create a scenario where the text analyzer performs poorly and you’ll identify the root cause of the performance problem. Finally, you’ll use the React memo function to prevent re-renders on the component when a parent changes, but the props to the child component do not change.

      By the end of this step, you’ll have a working component that you’ll use throughout the rest of the tutorial and an understanding of how parent re-rendering can create performance problems in child components.

      Building a Text Analyzer

      To begin, add a <textarea> element to App.js.

      Open App.js in a text editor of your choice:

      • nano src/components/App/App.js

      Then add a <textarea>input with a <label>. Place the label inside a <div> with a className of wrapper by adding the following highlighted code:

      performance-tutorial/src/components/App/App.js

      import React from 'react';
      import './App.css';
      
      function App() {
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Add Your Text Here:</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
              >
              </textarea>
            </label>
          </div>
        )
      }
      
      export default App;
      

      This will create an input box for the sample application. Save and close the file.

      Open App.css to add some styles:

      • nano src/components/App/App.css

      Inside App.css, add padding to the .wrapper class, then add a margin to the div elements. Replace the CSS with the following:

      performance-tutorial/src/components/App/App.css

      
      .wrapper {
          padding: 20px;
      }
      
      .wrapper div {
          margin: 20px 0;
      }
      

      This will add separation between the input and the data display. Save and close the file.

      Next, create a directory for the CharacterMap component. This component will analyze the text, calculate the frequency of each letter and symbol, and display the results.

      First, make the directory:

      • mkdir src/components/CharacterMap

      Then open CharacterMap.js in a text editor:

      • nano src/components/CharacterMap/CharacterMap.js

      Inside, create a component called CharacterMap that takes text as a prop and displays the text inside a <div>:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React from 'react';
      import PropTypes from 'prop-types';
      
      export default function CharacterMap({ text }) {
        return(
          <div>
            Character Map:
            {text}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        text: PropTypes.string.isRequired
      }
      

      Notice that you are adding a PropType for the text prop to introduce some weak typing.

      Add a function to loop over the text and extract the character information. Name the function itemize and pass the text as an argument. The itemize function is going to loop over every character several times and will be very slow as the text size increases. This will make it a good way to test performance:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        const letters = text.split('')
          .filter(l => l !== ' ')
          .reduce((collection, item) => {
            const letter = item.toLowerCase();
            return {
              ...collection,
              [letter]: (collection[letter] || 0) + 1
            }
          }, {})
        return letters;
      }
      
      export default function CharacterMap({ text }) {
        return(
          <div>
            Character Map:
            {text}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        text: PropTypes.string.isRequired
      }
      

      Inside itemize, you convert the text into an array by using .split on every character. Then you remove the spaces using the .filter method and use the .reduce method to iterate over each letter. Inside the .reduce method, use an object as the initial value, then normalize the character by converting it to lower case and adding 1 to the previous total or 0 if there was no previous total. Update the object with the new value while preserving previous values using the Object spread operator.

      Now that you have created an object with a count for each character, you need to sort it by the highest character. Convert the object to an array of pairs with Object.entries. The first item in the array is the character and the second item is the count. Use the .sort method to place the most common characters on top:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        const letters = text.split('')
          .filter(l => l !== ' ')
          .reduce((collection, item) => {
            const letter = item.toLowerCase();
            return {
              ...collection,
              [letter]: (collection[letter] || 0) + 1
            }
          }, {})
        return Object.entries(letters)
          .sort((a, b) => b[1] - a[1]);
      }
      
      export default function CharacterMap({ text }) {
        return(
          <div>
            Character Map:
            {text}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        text: PropTypes.string.isRequired
      }
      

      Finally, call the itemize function with the text and display the results:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      
      import React from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        const letters = text.split('')
          .filter(l => l !== ' ')
          .reduce((collection, item) => {
            const letter = item.toLowerCase();
            return {
              ...collection,
              [letter]: (collection[letter] || 0) + 1
            }
          }, {})
        return Object.entries(letters)
          .sort((a, b) => b[1] - a[1]);
      }
      
      export default function CharacterMap({ text }) {
        return(
          <div>
            Character Map:
            {itemize(text).map(character => (
              <div key={character[0]}>
                {character[0]}: {character[1]}
              </div>
            ))}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        text: PropTypes.string.isRequired
      }
      

      Save and close the file.

      Now import the component and render inside of App.js. Open App.js:

      • nano src/components/App/App.js

      Before you can use the component, you need a way to store the text. Import useState then call the function and store the values on a variable called text and an update function called setText.

      To update the text, add a function to onChange that will pass the event.target.value to the setText function:

      performance-tutorial/src/components/App/App.js

      import React, { useState } from 'react';
      import './App.css';
      
      function App() {
        const [text, setText] = useState('');
      
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Your Text</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
                onChange={event => setText(event.target.value)}
              >
              </textarea>
            </label>
          </div>
        )
      }
      
      export default App;
      

      Notice that you are initializing useState with an empty string. This will ensure that the value you pass to the CharacterMap component is always a string even if the user has not yet entered text.

      Import CharacterMap and render it after the <label> element. Pass the text state to the text prop:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { useState } from 'react';
      import './App.css';
      
      import CharacterMap from '../CharacterMap/CharacterMap';
      
      function App() {
        const [text, setText] = useState('');
      
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Your Text</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
                onChange={event => setText(event.target.value)}
              >
              </textarea>
            </label>
            <CharacterMap text={text} />
          </div>
        )
      }
      
      export default App;
      

      Save the file. When you do, the browser will refresh and when you add text, you’ll find the character analysis after the input:

      Input with results below

      As shown in the example, the component performs fairly well with a small amount of text. With every keystroke, React will update the CharacterMap with new data. But performance locally can be misleading. Not all devices will have the same memory as your development environment.

      Testing Performance

      There are multiple ways to test performance of your application. You can add large volumes of text or you can set your browser to use less memory. To push the component to a performance bottleneck, copy the Wikipedia entry for GNU and paste it in the text box. Your sample may be slightly different depending on how the Wikipedia page is edited.

      After pasting the entry into your text box, try typing the additional letter e and notice how long it takes to display. There will be a significant pause before the character map updates:

      Animation showing the delay when typing

      If the component is not slow enough and you are using Firefox, Edge, or some other browser, add more text until you notice a slowdown.

      If you are using Chrome, you can throttle the CPU inside the performance tab. This is a great way to emulate a smartphone or an older piece of hardware. For more information, check out the Chrome DevTools documentation.

      Performance Throttling in Chrome DevTools

      If the component is too slow with the Wikipedia entry, remove some text. You want to receive a noticable delay, but you do not want to make it unusably slow or to crash your browser.

      Preventing Re-Rendering of Child Components

      The itemize function is the root of the delay identified in the last section. The function does a lot of work on each entry by looping over the contents several times. There are optimizations you can perform directly in the function itself, but the focus of this tutorial is how to handle component re-rendering when the text does not change. In other words, you will treat the itemize function as a function that you do not have access to change. The goal will be to run it only when necessary. This will show how to handle performance for APIs or third-party libraries that you can’t control.

      To start, you will explore a situation where the parent changes, but the child component does not change.

      Inside of App.js, add a paragraph explaining how the component works and a button to toggle the information:

      performance-tutorial/src/components/App/App.js

      import React, { useReducer, useState } from 'react';
      import './App.css';
      
      import CharacterMap from '../CharacterMap/CharacterMap';
      
      function App() {
        const [text, setText] = useState('');
        const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
      
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Your Text</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
                onChange={event => setText(event.target.value)}
              >
              </textarea>
            </label>
            <div>
              <button onClick={toggleExplanation}>Show Explanation</button>
            </div>
            {showExplanation &&
              <p>
                This displays a list of the most common characters.
              </p>
            }
            <CharacterMap text={text} />
          </div>
        )
      }
      
      export default App;
      

      Call the useReducer Hook with a reducer function to reverse the current state. Save the output to showExplanation and toggleExplanation. After the <label>, add a button to toggle the explanation and a paragraph that will render when showExplanation is truthy.

      Save and exit the file. When the browser refreshes, click on the button to toggle the explanation. Notice how there is a delay.

      Delay when toggling the Explanation

      This presents a problem. Your users shouldn’t encounter a delay when they are toggling a small amount of JSX. The delay occurs because when the parent component changes—App.js in this situation—the CharacterMap component is re-rendering and re-calculating the character data. The text prop is identical, but the component still re-renders because React will re-render the entire component tree when a parent changes.

      If you profile the application with the browser’s developer tools, you’ll discover that the component re-renders because the parent changes. For a review of profiling using the developer tools, check out How To Debug React Components Using React Developer Tools.

      Parent component re-renders in browser developer tools

      Since CharacterMap contains an expensive function, it should only re-render it when the props change.

      Open CharacterMap.js:

      • nano src/components/CharacterMap/CharacterMap.js

      Next, import memo, then pass the component to memo and export the result as the default:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { memo } from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        ...
      }
      
      function CharacterMap({ text }) {
        return(
          <div>
            Character Map:
            {itemize(text).map(character => (
              <div key={character[0]}>
                {character[0]}: {character[1]}
              </div>
            ))}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        text: PropTypes.string.isRequired
      }
      
      export default memo(CharacterMap);
      

      Save the file. When you do, the browser will reload and there will no longer be a delay after you click the button before you get the result:

      No delay when toggling the explanation in the test app

      If you look at the developer tools, you’ll find that the component no longer re-renders:

      Component did not re-render

      The memo function will perform a shallow comparison of props and will re-render only when the props change. A shallow comparison will use the === operator to compare the previous prop to the current prop.

      It’s important to remember that the comparison is not free. There is a performance cost to check the props, but when you have a clear performance impact such as an expensive calculation, it is worth it to prevent re-renders. Further, since React performs a shallow comparison, the component will still re-render when a prop is an object or a function. You will read more about handling functions as props in Step 3.

      In this step, you created an application with a long, slow calculation. You learned how parent re-rendering will cause a child component to re-render and how to prevent the re-render using memo. In the next step, you’ll memoize actions in a component so that you only perform actions when specific properties change.

      Step 2 — Caching Expensive Data Calculations with useMemo

      In this step, you’ll store the results of slow data calculations with the useMemo Hook. You’ll then incorporate the useMemo Hook in an existing component and set conditions for data re-calculations. By the end of this step, you’ll be able to cache expensive functions so that they will only run when specific pieces of data change.

      In the previous step, the toggled explanation of the component was part of the parent. However, you could instead add it to the CharacterMap component itself. When you do, CharacterMap will have two properties, the text and showExplanation, and it will display the explanation when showExplanation is truthy.

      To start, open CharacterMap.js:

      • nano src/components/CharacterMap/CharacterMap.js

      Inside of CharacterMap, add a new property of showExplanation. Display the explanation text when the value of showExplanation is truthy:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { memo } from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        ...
      }
      
      function CharacterMap({ showExplanation, text }) {
        return(
          <div>
            {showExplanation &&
              <p>
                This display a list of the most common characters.
              </p>
            }
            Character Map:
            {itemize(text).map(character => (
              <div key={character[0]}>
                {character[0]}: {character[1]}
              </div>
            ))}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        showExplanation: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired
      }
      
      export default memo(CharacterMap);
      

      Save and close the file.

      Next, open App.js:

      • nano src/components/App/App.js

      Remove the paragraph of explanation and pass showExplanation as a prop to CharacterMap:

      performance-tutorial/src/components/App/App.js

      import React, { useReducer, useState } from 'react';
      import './App.css';
      
      import CharacterMap from '../CharacterMap/CharacterMap';
      
      function App() {
        const [text, setText] = useState('');
        const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
      
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Your Text</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
                onChange={event => setText(event.target.value)}
              >
              </textarea>
            </label>
            <div>
              <button onClick={toggleExplanation}>Show Explanation</button>
            </div>
            <CharacterMap showExplanation={showExplanation} text={text} />
          </div>
        )
      }
      
      export default App;
      

      Save and close the file. When you do, the browser will refresh. If you toggle the explanation, you will again receive the delay.

      Delay when toggling explanation

      If you look at the profiler, you’ll discover that the component re-rendered because the showExplanation prop changed:

      Re-render because prop changed

      The memo function will compare props and prevent re-renders if no props change, but in this case the showExplanation prop does change, so the whole component will re-render and the component will re-run the itemize function.

      In this case, you need to memoize specific parts of the component, not the whole component. React provides a special Hook called useMemo that you can use to preserve parts of your component across re-renders. The Hook takes two arguments. The first argument is a function that will return the value you want to memoize. The second argument is an array of dependencies. If a dependency changes, useMemo will re-run the function and return a value.

      To implement useMemo, first open CharacterMap.js:

      • nano src/components/CharacterMap/CharacterMap.js

      Declare a new variable called characters. Then call useMemo and pass an anonymous function that returns the value of itemize(text) as the first argument and an array containing text as the second argument. When useMemo runs, it will return the result of itemize(text) to the characters variable.

      Replace the call to itemize in the JSX with characters:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { memo, useMemo } from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        ...
      }
      
      function CharacterMap({ showExplanation, text }) {
        const characters = useMemo(() => itemize(text), [text]);
        return(
          <div>
            {showExplanation &&
              <p>
                This display a list of the most common characters.
              </p>
            }
            Character Map:
            {characters.map(character => (
              <div key={character[0]}>
                {character[0]}: {character[1]}
              </div>
            ))}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        showExplanation: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired
      }
      
      export default memo(CharacterMap);
      

      Save the file. When you do, the browser will reload and there will be no delay when you toggle the explanation.

      Animation showing no delay when toggling explanation

      If you profile the component, you will still find that it re-renders, but the time it takes to render will be much shorter. In this example it took .7 milliseconds compared to 916.4 milliseconds without the useMemo Hook. That’s because React is re-rendering the component, but it is not re-running the function contained in the useMemo Hook. You’re able to preserve the result while still allowing other parts of the component to update:

      Developer tools profile showing that the component renders in .7ms

      If you change the text in the textbox, there will still be a delay because the dependency—text—changed, so useMemo will re-run the function. If it did not re-run, you would have old data. The key point is that it only runs when the data it needs changes.

      In this step, you memoized parts of your component. You isolated an expensive function from the rest of the component and used the useMemo Hook to run the function only when certain dependencies change. In the next step, you’ll memoize functions to prevent shallow comparison re-renders.

      Step 3 — Managing Function Equality Checks with useCallback

      In this step, you’ll handle props that are difficult to compare in JavaScript. React uses strict equality checking when props change. This check determines when to re-run Hooks and when to re-render components. Since JavaScript functions and objects are difficult to compare, there are situations where a prop would be effectively the same, but still trigger a re-render.

      You can use the useCallback Hook to preserve a function across re-renders. This will prevent unnecessary re-renders when a parent component recreates a function. By the end of this step, you’ll be able to prevent re-renders using the useCallback Hook.

      As you build your CharacterMap component, you may have a situation where you need it to be more flexible. In the itemize function, you always convert the character to lower case, but some consumers of the component may not want that functionality. They may want to compare upper and lowercase characters or want to convert all characters to upper case.

      To facilitate this, add a new prop called transformer that will change the character. The transformer function will be anything that takes a character as an argument and returns a string of some sort.

      Inside of CharacterMap, add transformer as a prop. Give it a PropType of function with a default of null:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { memo, useMemo } from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text){
        const letters = text.split('')
          .filter(l => l !== ' ')
          .reduce((collection, item) => {
            const letter = item.toLowerCase();
            return {
              ...collection,
              [letter]: (collection[letter] || 0) + 1
            }
          }, {})
        return Object.entries(letters)
          .sort((a, b) => b[1] - a[1]);
      }
      
      function CharacterMap({ showExplanation, text, transformer }) {
        const characters = useMemo(() => itemize(text), [text]);
        return(
          <div>
            {showExplanation &&
              <p>
                This display a list of the most common characters.
              </p>
            }
            Character Map:
            {characters.map(character => (
              <div key={character[0]}>
                {character[0]}: {character[1]}
              </div>
            ))}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        showExplanation: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired,
        transformer: PropTypes.func
      }
      
      CharacterMap.defaultProps = {
        transformer: null
      }
      
      export default memo(CharacterMap);
      

      Next, update itemize to take transformer as an argument. Replace the .toLowerCase method with the transformer. If transformer is truthy, call the function with the item as an argument. Otherwise, return the item:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { memo, useMemo } from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text, transformer){
        const letters = text.split('')
          .filter(l => l !== ' ')
          .reduce((collection, item) => {
            const letter = transformer ? transformer(item) : item;
            return {
              ...collection,
              [letter]: (collection[letter] || 0) + 1
            }
          }, {})
        return Object.entries(letters)
          .sort((a, b) => b[1] - a[1]);
      }
      
      function CharacterMap({ showExplanation, text, transformer }) {
          ...
      }
      
      CharacterMap.propTypes = {
        showExplanation: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired,
        transformer: PropTypes.func
      }
      
      CharacterMap.defaultProps = {
        transformer: null
      }
      
      export default memo(CharacterMap);
      

      Finally, update the useMemo Hook. Add transformer as a dependency and pass it to the itemize function. You want to be sure that your dependencies are exhaustive. That means you need to add anything that might change as a dependency. If a user changes the transformer by toggling between different options, you’d need to re-run the function to get the correct value.

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { memo, useMemo } from 'react';
      import PropTypes from 'prop-types';
      
      function itemize(text, transformer){
        ...
      }
      
      function CharacterMap({ showExplanation, text, transformer }) {
        const characters = useMemo(() => itemize(text, transformer), [text, transformer]);
        return(
          <div>
            {showExplanation &&
              <p>
                This display a list of the most common characters.
              </p>
            }
            Character Map:
            {characters.map(character => (
              <div key={character[0]}>
                {character[0]}: {character[1]}
              </div>
            ))}
          </div>
        )
      }
      
      CharacterMap.propTypes = {
        showExplanation: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired,
        transformer: PropTypes.func
      }
      
      CharacterMap.defaultProps = {
        transformer: null
      }
      
      export default memo(CharacterMap);
      

      Save and close the file.

      In this application, you don’t want to give users the ability to toggle between different functions. But you do want the characters to be lower case. Define a transformer in App.js that will convert the character to lower case. This function will never change, but you do need to pass it to the CharacterMap.

      Open App.js:

      • nano src/components/App/App.js

      Then define a function called transformer that converts a character to lower case. Pass the function as a prop to CharacterMap:

      performance-tutorial/src/components/CharacterMap/CharacterMap.js

      import React, { useReducer, useState } from 'react';
      import './App.css';
      
      import CharacterMap from '../CharacterMap/CharacterMap';
      
      function App() {
        const [text, setText] = useState('');
        const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
        const transformer = item => item.toLowerCase();
      
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Your Text</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
                onChange={event => setText(event.target.value)}
              >
              </textarea>
            </label>
            <div>
              <button onClick={toggleExplanation}>Show Explanation</button>
            </div>
            <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} />
          </div>
        )
      }
      
      export default App;
      

      Save the file. When you do, you will find that the delay has returned when you toggle the explanation.

      Animation showing a delay when toggling explanation

      If you profile the component, you will find that the component re-renders because the props change and the Hooks changed:

      Profile for transformer

      If you look closely, you’ll find that the showExplanation prop changed, which makes sense because you clicked the button, but the transformer prop also changed.

      When you made a state change in App by clicking on the button, the App component re-rendered and redeclared the transformer. Even though the function is the same, it is not referentially identical to the previous function. That means it’s not strictly identical to the previous function.

      If you open the browser console and compared identical functions, you’d find that the comparison is false, as shown in the following code block:

      const a = () = {};
      const b = () = {};
      
      a === a
      // This will evaluate to true
      
      a === b
      // This will evaluate to false
      

      Using the === comparison operator, this code shows that two functions are not considered equal, even if they have the same values.

      To avoid this problem, React provides a Hook called useCallback. The Hook is similar to useMemo: it takes a function as the first argument and an array of dependencies as the second argument. The difference is that useCallback returns the function and not the result of the function. Like the useMemo Hook, it will not recreate the function unless a dependency changes. That means that the useMemo Hook in CharacterMap.js will compare the same value and the Hook will not re-run.

      Inside of App.js, import useCallback and pass the anonymous function as the first argument and an empty array as the second argument. You never want App to recreate this function:

      performance-tutorial/src/components/App/App.js

      import React, { useCallback, useReducer, useState } from 'react';
      import './App.css';
      
      import CharacterMap from '../CharacterMap/CharacterMap';
      
      function App() {
        const [text, setText] = useState('');
        const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
        const transformer = useCallback(item => item.toLowerCase(), []);
      
        return(
          <div className="wrapper">
            <label htmlFor="text">
              <p>Your Text</p>
              <textarea
                id="text"
                name="text"
                rows="10"
                cols="100"
                onChange={event => setText(event.target.value)}
              >
              </textarea>
            </label>
            <div>
              <button onClick={toggleExplanation}>Show Explanation</button>
            </div>
            <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} />
          </div>
        )
      }
      
      export default App;
      

      Save and close the file. When you do, you’ll be able to toggle the explanation without re-running the function.

      Animation showing no delay when toggling the explanation component

      If you profile the component, you’ll find that the Hook no longer runs:

      Image of the browser developer tools showing that the Hook does not run

      In this particular component, you do not actually need the useCallback Hook. You could declare the function outside of the component and it would never re-render. You should only declare functions inside of your component if they require some sort of prop or stateful data. But there are times when you need to create functions based on internal state or props and in those situations you can use the useCallback Hook to minimize re-renders.

      In this step, you preserved functions across re-renders using the useCallback Hook. You also learned how those functions will retain equality when compared as props or dependencies in a Hook.

      Conclusion

      You now have the tools to improve performance on expensive components. You can use memo, useMemo, and useCallback to avoid costly component re-renders. But all these strategies include a performance cost of their own. memo will take extra work to compare properties, and the Hooks will need to run extra comparisons on each render. Only use these tools when there is a clear need in your project, otherwise you risk adding in your own latency.

      Finally, not all performance problems require a technical fix. There are times when the performance cost is unavoidable—such as slow APIs or large data conversions—and in those situations you can solve the problem using design by either rendering loading components, showing placeholders while asynchronous functions are running, or other enhancements to the user experience.

      If you would like to read more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page.



      Source link