Optimizing ReactJS components

Getting ready

To understand when we should use the memo, we first need to understand how React works with rendering.

What is the memo for?

Since you already know how React works under the hood, let's move on to the applicability of the memo and take our applications to the next level. The memo will prevent React from re-rendering the child component if it hasn't changed when the parent component changes.

A clear example of the problem that memo solves in React is: whenever a parent component undergoes a change (whether in properties, state, css, etc.), the child components are also rendered again.

Now that we know what the memo is for within the React world, let's go for a hands on!

Hands on

Let's take the following code as an example:

// pages/Home.tsx

import { useState } from 'react';
import { api } from '../services/api';
import { Item } from '../components/Item';

function List() {
	const [items, setItems] = useState([]);

	async function handleClick() {
		const response = await api.get('/items')
		setItems(response);
	};

	return (
		<div>
			<div>
				{items.map((item) => (
					<Item key={item.title} item={item} />
				))}
			</div>

			<button type="button" onClick={handleClick}>Load items</button>
		</div>
	);
};
// components/Item.tsx

import { memo } from 'react';

interface ItemComponentProps {
	item: {
		title: string;
	}
};

function ItemComponent({ item }: ItemComponentProps) {
	return (
		<div>
			<h1>{item.title}</h1>
		</div>
	)
};

export const Item = memo(ItemComponent, (prevProps, nextProps) => {
	return Object.is(preProps.item, nextProps.item);
});

In this example, the following scenario takes place:

  1. When clicking the button, we'll make a call to the API and store the response in the local state of the component;
  2. When a local state is updated, React re-renders the Home page displaying an <Item /> for each record returned from the API;
  3. If we click the button to reload the data, the API will return us the most up-to-date data and consequently we'll have more data displayed on screen;
  4. Using memo, React will behave as fallows when it starts the process of creating the Virtual DOM for each child component:
    1. Have the properties of that component changed?
    2. If yes, I'll render those changes;
    3. If no, I will do nothing with this child component;

Shallow compare

Did you know that the code snippet {} === {} returns false in JavaScript? This is because the equality operator === considers the comparison of memory and not just of value. That is, if you compare two objects that occupy different memory locations but have the same value, JavaScript will identify that they are different because they occupy different memory locations. For that reason, the memo can still re-render the child components since prevProps, and next props can occupy different memory locations.

To solve this problem, memo gets a second parameter, a function in which we will define what it means for a property to have been changed or not for React. With this, we use Object.is() to do a deep comparison considering only the values of the object we received as a property in the component. If all values are equal, our function will return a true value, and React will not render the child component unnecessarily.

Use cases

Don't use memo prematurely without real need. Unnecessary use of memo can cause your application to lose performance because it has to execute all the memo logic even when it is unnecessary.

Memo is used in some main situations:

The useMemo hook

The use of useMemo prevents unnecessary heavy processing from being re-run along with the re-rendering of the component. Let's see an example of how we can apply this hook!

Hands on

Let's assume the following code:

interface CartShoppingProps {
	items: {
		id: string;
		price: string;
	};
};

export function CartShopping({ items }: CartShoppingProps) {
	const total = items.reduce((accumulator, item) => {
		return accumulator + item.price;
	}, 0);

	return (
		<h1>Total price: {total}</h1>
	);
};
  • If some change causes the <CartShopping /> component to be re-rendered, reduce() will be executed again even if the final value is the same;

  • With useMemo, React avoids running reduce() again if the result remains the same. See the example below:

    import { useMemo } from 'react';
    
    interface CartShoppingProps {
      items: {
        id: string;
        price: string;
      };
    };
    
    export function CartShopping({ items }: CartShoppingProps) {
      const total = useMemo(() => {
        return items.reduce((accumulator, item) => {
          return accumulator + item.price;
        }, 0);
      })
    
      return (
        <h1>Total price: {total}</h1>
      );
    };
    
  • In this scenario, if the component is re-rendered, but the values remain in the properties, reduce() will not run again, and the application will consume less processing;

  • With useMemo, React avoids a variable occupying a new memory location when we use it to pass on to a child component;

Use cases

  • Heavy processing;
  • Referential equality (when we pass information to a child component);

The useCallback hook

We use useCallback when we want to remember a function, not a value. For values we use useMemo.

When we create a function and this function is going to be passed on to the child components of our application, it is important that this function uses useCallback because every time a function is re-created in memory, React will re-render the component that receives the function, even if it has not changed.

The useCallback receives 2 parameters:

  1. The first is the function that will be managed by the useCallback hook;
  2. The second is an array of dependencies that will cause the function to be re-rendered;

Data formatting

Even though we use useMemo to avoid unnecessary calculations, React has comparison logic to identify whether calculations should be redone or not through useMemo. However, if I know when I should do the calculations, it is more interesting for me not to use useMemo so that it is possible to format the data at the interface build level and perform this only when I know we will need it (when getting the response from an API, for example).

Code splitting

When you generate the build of your React application, the process is: to take the entire import file tree and implement it in your bundle. In other words, the bundle contains all the code required for our application to run.

However, there is a scenario where we only want to import a file if some condition is met (e.g., display a formatted date if the user clicks to open a modal). This becomes a problem because the bundle code will include code that will be used only in a specific condition, and not always.

To solve this problem we can do lazy loading only when we need it. Here is some sample code for this implementation:

1. Import the method that works with lazy loading of code.

For applications with pure ReactJS:

import { lazy as lazyLoading } from 'react';

For applications with NextJS:

import lazyLoading from 'next/dynamic';

2. Turn your component into a lazy loading compatible component

const MyLazyComponent = lazyLoading(() => {
	return import('../components/MyComponent');
  }, {
    loading: () => <span>Loading...</span>, // here you can define a loading component to be shown when the application is "downloading" the component to be shown.
  });

And now, just use your component, and it will be loaded only when it actually needs to be used.

Virtualization

Let's imagine a list of 1,000 items. Once a user accesses this list, it will take a lot of effort for the browser to render all the items in the list. Considering that not all 1,000 items will fit inside the end user's browser view, we can work with a virtualization solution, in which React will display only the elements that the user has a field-of-view view of, thus drastically reducing the processing to display the full list.

For this, I like to use a library called react-virtualized. So let's go through step by step how to implement this functionality!

Hands on

Install the library:

yarn add react-virtualized && yarn add @types/react-virtualized -D

Create a component that will be responsible for rendering the list:

import { List, AutoSizer, ListRowRenderer } from 'react-virtualized';

interface MyComponentProps {
	items: string[];
};

export function MyComponent({ items }: MyComponentProps) {
	const rowRenderer: ListRowRenderer = ({ index, key, style }) => {
		return (
			<div key={key} style={style}>
				<li>{items[index]}</li>
			</div>
		);
	};

	return (
		<AutoSizer>
	    {({height, width}) => (
	      <List
	        height={height}
	        width={width}
	        rowCount={list.length}
	        rowHeight={20}
	        rowRenderer={rowRenderer}
					overscanRowCount={5} // prefetch next N items to be shown
	      />
	    )}
	  </AutoSizer>
	);
};

With this, we will always show the number of items needed to fill the client's screen, and we will always have the next 5 items pre-loaded to be displayed if the user scrolls to see more records. This makes the rendering of many elements more performant.

Bundle analyzer with NextJS

When we generate our build for production, it can get very heavy and get in the way of other steps in the deployment. But, how can we solve this? How about we analyze the size of our bundle and identify which modules have the largest size so we can act upon this and optimize our deployment? This is possible and it's very easy to apply in a NextJS application! Let's see how to do it in practice 😉

Hands on

Installing the library:

yarn add @next/bundle-analyzer

In the root of your project, create a file called next.config.js with the following information:

const withBundleAnalyzer = require('@next/bundle-analyzer')({
	enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({});

Now, to analyze the bundle generated in your project build, just run the following command in your terminal:

ANALYZE=true yarn build

The command will open a window in your browser to show you which packages weigh the most in your bundle.

Finishing up

I hope you have enjoyed this content and that you have learned something to take you to the next level and perform your projects.

This post took a few hours to complete and if you have any questions, leave them in the comments below and I or any other member that knows the answer will answer them as soon as possible.

Finally, here are some references for you to further explore this performance-oriented content in React:

Stay in the loop!

Got a thirst for more such insightful articles on programming and tech? Sign up for my newsletter here and stay informed!