Key attribute in React

Key attribute in React

This article if a follow up to my previous article Demystifying re-rendering, diffing, and reconciliation in React.

React's key attribute is a powerful tool that often goes unnoticed or misunderstood by developers. In this article, we'll delve into the importance of the key attribute when rendering lists in React components. We'll explore why it's crucial for both dynamic and static arrays, how it enhances performance, and the potential pitfalls of neglecting this seemingly small detail. By the end, you'll have a solid understanding of why the key attribute is a must-have in your React toolkit.

Introduction

So Far we were focusing on how React renders a single component, but what if we have a component that returns an array of elements, how does React will make the diffing and reconciliation process in that case?

In some cases React components may return an array of elements e.g. when your components return React.Fragment or <></> syntax, or when you return an array of elements from map function.

Copy
1const App = () => {
2  return (
3    <>
4      <h1>My App</h1>
5      <p>Some text</p>
6    </>
7  );
8};
Copy
1const App = () => {
2  const items = ['item1', 'item2', 'item3'];
3
4  return items.map((item,idx) => (
5        <p key={idx}>{item}</p>
6      ))
7};

In this article we will see how React renders these components and how it handles the re-rendering process.

The process of Diffing and Reconciliation is the same for both cases, so let's see how it works.

How React compares two arrays of elements.

Let's see how React renders the following component:

Copy
1const App = () => {
2  const [show, setShow] = useState(true);
3  return (
4    <>
5      <h1>My App</h1>
6      <p>Some text</p>
7      {show ? <p>Some other text</p> : <span>Some other text</span>}
8    </>
9  );
10};

The component will return the following array of elements:

Copy
1
2[
3  {
4    key: null,
5    props: { children: 'My App' },
6    ref: null,
7    type: 'h1'
8  },
9  {
10    key: null,
11    props: { children: 'Some text' },
12    ref: null,
13    type: 'p'
14  },
15    {
16        key: null,
17        props: { children: 'Some other text' },
18        ref: null,
19        type: 'p'
20    }
21]

When the show state change to false, we know that React by default will re-renders all nested components.

The new array of elements will be:

Copy
1
2[
3  {
4    key: null,
5    props: { children: 'My App' },
6    ref: null,
7    type: 'h1'
8  },
9  {
10    key: null,
11    props: { children: 'Some text' },
12    ref: null,
13    type: 'p'
14  },
15    {
16        key: null,
17        props: { children: 'Some other text' },
18        ref: null,
19        type: 'span'
20    }
21]

React now want to know which element should it update, which element should it remove and which element should it add. So it will compare the two arrays and it will start with the first item in the array, and then the second item and so on.

  1. The types are the same for the fist item, then it will be updated "re-rendered".
  2. The types are the same for the second item, then it will be updated "re-rendered".
  3. The types are different for the third item, then it will be removed and a new item will be added.

This is how React compares two arrays of elements.

What is the key attribute and why we should use it?

key in React is used very often, React enforce us when we render a list of elements to add a key to each element. And that key should be unique for each element.

Adding a key to dynamic list of elements, helping React to identify each element when the component re-render, This help React to keep track of the existed elements, and the if the list has new elements, React will know which elements should be added. A common misconception is that React uses the key for performance reasons, but that's not true, React uses the key to identify the element during re-rendering. React by default will re-render all the elements in the list unless we use React.memo to memoize the elements.

Note: When giving a key to an element, it should be unique for each element in the array, but it should persist between re-renders, even if the dynamic list gets shuffled, appended or prepended.

To know why the above Note is very important let's see how React compares arrays after re-rendering.

Copy
1
2
3const persons = [
4    {
5        id: 1,
6        name: 'John'
7    },
8    {
9        id: 2,
10        name: 'Jane'
11    },
12    {
13        id: 3,
14        name: 'Jack'
15    }
16]
17const App = () => {
18  ...
19  return persons.map((person,idx) => (
20        <Card keys={idx} title={person.name} />
21      ));
22};

In that case we want the Card element to not re-render in each re-rendering, so we can use React.memo to memoize the Card element.

Copy
1
2
3const MemoizedCard = React.memo(Card);
4
5const App = () => {
6  ...
7  ...
8  return persons.map((person,idx) => (
9        <MemoizedCard  keys={idx} title={person.name} />
10      ));
11};

When React build the array of elements, the result will be:

Copy
1
2[
3    {
4        key: 1,
5        props: { title: 'John' },
6        ref: null,
7        type: MemoizedCard
8    },
9    {
10        key: 2,
11        props: { title: 'Jane' },
12        ref: null,
13        type: MemoizedCard
14    },
15    {
16        key: 3,
17        props: { title: 'Jack' },
18        ref: null,
19        type: MemoizedCard
20    }
21]

Now all the elements in the array has the same type, when the App component re-renders, nothing will be changed in the array of elements, since types, props, and keys are the same during the re-render and each element is Memoized, so React will not re-render the MemoziedCard component.

Suppose when the App re-renders, it changed the order of the persons array to be:

Copy
1
2
3const persons = [
4    {
5        id: 2,
6        name: 'Jane'
7    },
8    {
9        id: 3,
10        name: 'Jack'
11    },
12    {
13        id: 1,
14        name: 'John'
15    }
16]
17
18// this will be transformed to:
19
20[
21     {
22        key: 2,
23        props: { title: 'Jane' },
24        ref: null,
25        type: MemoizedCard
26    },
27    {
28        key: 3,
29        props: { title: 'Jack' },
30        ref: null,
31        type: MemoizedCard
32    }
33    {
34        key: 1,
35        props: { title: 'John' },
36        ref: null,
37        type: MemoizedCard
38    },
39]

In that way React will compare the two arrays and it will find the following:

  1. key=0 : the type of the element is the same, but props are different, so it will be re-rendered.
  2. key=1 : the type of the element is the same, but props are different, so it will be re-rendered.
  3. key=2 : the type of the element is the same, but props are different, so it will be re-rendered.

Despite of nothing in the MemoizedCard component has changed, React will re-render it, because it doesn't know that the order of the array has changed.

Here the key attribute come to help, if we give each element a unique key that will persist between re-renders, React will know that the order of the array has changed, and it will not re-render the MemoizedCard component, It will just re-order the elements in the Virtual DOM.

Copy
1  ...
2  ...
3  return persons.map((person,idx) => (
4        <MemoizedCard  keys={person.id} title={person.name} />
5      ));

In that way nothing will be re-rendered, because React will know that the order of the array has changed, and it will re-ordered these elements in the Virtual DOM without re-rendering them.

Great Right!

You should't use the idx as a key, because it will not persist between re-renders, and it will not help React to know that the order of the array has changed. use it only in one case, when your list is static and will not change during the life cycle of the component.

Why React enforce to add key when rendering a dynamic list of elements, but it doesn't enforce to add key when rendering a static list of elements?

Did you wonder before why React does't enforce us to add a key when returning a fragment of elements from a component?

Copy
1const App = () => {
2  ...
3  return <>
4  <Input ... />
5  <Input ... />
6  <Input ... />
7  </>
8};

And why It enforce us to add a key when we returning an array of elements from a map function?

Copy
1const App = () => {
2  const list = ['item1', 'item2', 'item3'];
3  ...
4  return list.map((item,idx) => (
5        <Input ... key={idx} />
6      ));
7};

Actually Both are the same case thing when transforming them, React will build an array of three Input elements in both cases.

Copy
1
2[
3  {
4    props: { ... },
5    ref: null,
6    type: 'Input'
7  },
8  {
9    props: { ... },
10    ref: null,
11    type: 'Input'
12  },
13  {
14    props: { ... },
15    ref: null,
16    type: 'Input'
17  }
18]

So What is the difference between the two cases? why React enforce us to add a key in the second case but not in the first case?

The difference is that, in the second case React can't know if that array will be updated during re-rendres or not, maybe it's order change or maybe it's length change, so React enforce us to add a key to help it to know if the array has changed and for precautionary measure.

But in the first case React know that the array will not change during re-renders, so it doesn't enforce us to add a key.

Key attribute use cases

Let's see some use cases of the key attribute.

Copy
1const App = ()=>{
2  const [isAdmin, setIsAdmin] = useState(false);
3  return <>
4   {isAdmin ? <Input placeholder="Admin ID" /> : <Input placeholder="User ID" />}
5   <Checkbox label="are you admin?" onChange={()=>setIsAdmin(!isAdmin)} />
6  </>
7}
8
9// this will be transormed to:
10
11[
12    {
13        key: null,
14        props: { placeholder: 'User ID' },
15        ref: null,
16        type: Input
17    },
18    {
19        key: null,
20        props: { label: 'are you admin?' },
21        ref: null,
22        type: Checkbox
23    }
24]

In the above example, When the isAdmin state change to true, React will re-render the App component.

Copy
1
2[
3    {   
4        key: null,
5        props: { placeholder: 'Admin ID' },
6        ref: null,
7        type: Input
8    },
9    { 
10        key: null,
11        props: { label: 'are you admin?' },
12        ref: null,
13        type: Checkbox
14    }
15]

When React compare the first element it will see that the type is the same, but the props are different, so it will re-render the Input component.

And the input value will be preserved during re-rendering because the component did't unmounted and mounted again, it just re-rendered.

That is cool, right. But what if we did't want this behavior, what if we want to unmount the Input component when the isAdmin state change to true, and mount it again when the isAdmin state change to false.

So In every re-render the value of the input will be reset to empty string.

The answer is to add a key attribute to the Input component.

Copy
1const App = ()=>{
2  const [isAdmin, setIsAdmin] = useState(false);
3  return <>
4   {isAdmin ? <Input placeholder="Admin ID" key="admin" /> : <Input placeholder="User ID" key="user" />}
5   <Checkbox label="are you admin?" onChange={()=>setIsAdmin(!isAdmin)} />
6  </>
7}
8
9// this will be transormed to:
10
11[
12    {
13        key: "user",
14        props: { placeholder: 'User ID' },
15        ref: null,
16        type: Input
17    },
18    {
19        props: { label: 'are you admin?' },
20        ref: null,
21        type: Checkbox
22    }
23]
24
25// and when the isAdmin state change to true, it will be transormed to:
26
27[
28    {
29        key: "admin",
30        props: { placeholder: 'Admin ID' },
31        ref: null,
32        type: Input
33    },
34    {
35        props: { label: 'are you admin?' },
36        ref: null,
37        type: Checkbox
38    }
39]

When React compare the two arrays, the first element's type is the same, but the key is different, so React will unmount the Input component and mount it again with the new key.

And the input value will be reset to empty string because the component unmounted and mounted again.

The key if very crucial to React, if the key change for an element React will unmount it and mount it again even if the element is memoized using React.memo.

Conclusion

In this article we saw how React compares two arrays of elements, and how it uses the key attribute to identify each element during re-rendering. We also saw how we can use React.memo to memoize the elements in the array, and how we can use the key attribute to help React to know that the order of the array has changed.