Loading Inter font-family
Loading Josephine Sans font-family
Loading Hallywood font-family

Case Study: Mistake with react keys can kill the performance

Showrin Barua
Showrin Barua / March 09, 2022
7 min read----

Case Study: Mistake with react keys can kill the performance by Showrin

Overview

We all are familiar with the following warning from React. Right?

missing key warning in react

When we try to render a list in react, we all more or less face this issue. React suggests passing a unique key to every list item to track which components have changed, are added, or removed.

One thing to focus on here is It should be a unique key.

If we render a list that is coming from the Database table, then it should have a unique ID for each item (primary keys). We can use this ID as a key then.

Sometimes it’s possible to not have any unique stable ID for the list item. Suppose, you got an API response like the following:

{
	"id": 1,
	"title": "Case Study: Mistake with react keys can kill the performance",
	"sub_title": "When using UUID or Math.random() or any other library for react-keys, a slight mistake is killing your app performance.",
	"tags": ["react", "lists", "keys", "performance"],
}

Here if we want to render the tags list, then we don’t have any unique ID for the items. In that case, as the last resort, we can use the index as the key of these items.

But React doesn’t recommend index as key if the order of items may change. If the tags list changes like ["lists", "keys", "react", "performance"], (if you notice, you can see the order of "react" has changed), the index is not recommended to be used as a key. Cause It can negatively impact your performance. If you are interested to know how, here’s a detailed article - Index as a key is an anti-pattern.

So in this situation, what should we do? I think the first option that came to your mind is Math.random() or new Date(), isn’t it?

Also, there are some libraries that provide unique IDs, such as UUID. It’s a mostly used package for unique IDs.

When we are using these ways to make our app performant, many of us do a slight mistake that will decrease our app performance immensely. In this article, I am going to talk about that mistake.

The Mistake

In the above, we’ve listed 3 ways to generate a unique id for the key. But if you notice, you can see they all are functions. You have to invoke them and in return, they will give you a unique number or string. Like the following:

tags.map((tag) => <Tag key={uuid()} content={tag} />);

tags.map((tag) => <Tag key={Math.random()} content={tag} />);

tags.map((tag) => <Tag key={new Date()} content={tag} />);

So in every iteration, these functions will be invoked and will pass a unique id to the key.

But the problem is these functions are invoked in each re-render and generate different IDs every time. As a result, during a re-render, even no prop of the Tag component has changed, the component will re-render as its key gets changed. In worst cases, it may result in an infinite loop and the following error will occur.

maximum call stack exceed error in react

Yeah, while trying to increase performance, we are decreasing it a lot just for a silly mistake.

Solution

  1. As the first choice, it's always recommended to use ID from the API response (if possible).
  2. If the above is not possible, then we can generate the ID (using UUID or Math.random() or in other way) in the outside of the functional component and then use it.
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import Tag from './Tag';

const tags = ['apple', 'banana', 'orange'];
const tagKeys = [uuidv4(), uuidv4(), uuidv4()];

const TagList = () => {
  return tags.map((tag, index) => <Tag key={tagKeys[index]} content={tag} />);
};
  1. The above solution isn’t much useful. Because in this dynamic world, there are very rare cases where we define tags manually like this. Otherwise, most of the time we get these tags from the API or props. In both cases, the length of tags will be dynamic. So we have to generate these keys inside the component, as we haven’t access to tags.length from the outside. Like the following:
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import Tag from './Tag';

const TagList = (props) => {
  const { tags } = props;
  const tagKeys = tags.map((_) => uuidv4());

  return tags.map((tag, index) => <Tag key={tagKeys[index]} content={tag} />);
};
  1. But in the above solution, we are again losing our main goal. Because, every time the TagList component gets rendered, tagKeys will regenerate. And even if no prop of Tag has changed (except key), it will re-render. To prevent this situation, we have to memorize the keys that are generated at the first render and use them in every re-renders later. Here, useMemo comes to the rescue.`
import React, { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import Tag from './Tag';

const TagList = (props) => {
  const { tags } = props;
  const tagKeys = tags.map((_) => useMemo(() => uuidv4(), []));

  return tags.map((tag, index) => <Tag key={tagKeys[index]} content={tag} />);
};

Here what, we have done is, memoize the keys at first render. As we are passing [] as the dependency list of useMemo(), keys won’t change until TagList gets unmounted and then re-mounted.

More Dynamic Solution

The solution provided at point 4 is fully working. But you already noticed that if we want to implement this solution through our app, we have to write the following piece of code again and again in each list-type component.

import { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';

const tagKeys = tags.map((_) => useMemo(() => uuidv4(), []));

So, why not make it a separate function. In react-way, we can call it a hook, most specifically a custom hook. In the following, a custom hook called useUUID is written.

import { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';

const useUUID = (arraySize = 1) => {
  const ids = [];

  for (let i = 0; i < arraySize; i = i + 1) {
    ids.push(uuidv4());
  }

  return useMemo(() => ids, []);
};

export default useUUID;

And then we can use it in all the list-type components like the following.

import React from 'react';
import Tag from './Tag';
import useUUID from './useUUID';

const TagList = (props) => {
  const { tags } = props;
  const tagKeys = useUUID(tags.length);

  return tags.map((tag, index) => <Tag key={tagKeys[index]} content={tag} />);
};

Here’s a sample implementation of our discussions in this article. Please, have a look at it and feel free to play with it.

The Takeaway

Generating unique keys is sometimes very challenging in react. That’s why we have to take the help of some third-party libraries like UUID or built-in libraries like Math.random() and Date().

When using those libraries, we have to memoize the generated ids in the first render. Otherwise, these function will return different ids in each re-render which will cause unnecessary re-render of the component (the component that has a key attached with).

ReactPerformanceOptimization

Stay Tuned To Get Latests