Why do I need key when mapping through list of items in React?

Published on: 11/16/2023

single key and map pointing a way to the open treasure filled with gold and precious stones in watercolor

Contents

  1. Why do I need key when displaying a list of items?
  2. Forcing a re-render
  3. Rendering a fragment with siblings list with a key
  4. Example with components, memo and keys
  5. Interactive Example
  6. Conclusion

Why do I need key when displaying a list of items?

React is sophisticated tool and helps with a lot of low level stuff, but contrary to what AI people will tell you it cannot read your mind just yet.

When rendering the page React knows what it needs to re-render and more importantly what to destroy and add to the DOM. It does this by comparing previous state of the DOM tree to the current one. It creates it by comparing type, key (if it exists) and props of the components with the new type, key and props when the state changes. The difference between old and new versions tells it which parts of the application need to be re-rendered and how. This is simplified version of the reconciliation algorithm. You can read more about it in the legacy documentation.

React compares the elements by shallow comparison using Object.is as it is safer way for React than using ===. The differences between them is that Object.is treats NaNs as the same values and 0 and -0as different. See MDN documentation on Object.is if you want to know more.

Where keys play an instrumental (you could even dare to say the key) role is the described reconciliation mode. When it happens React verifies if the type of the component changed and the old one should be removed (unmounted) from the DOM or if it needs to update the component (when props change). It uses createElement function to add new nodes to DOM. See: rendering lists

To dispel a bit of magic from JSX let's take a peek at it's insides. The following examples are equivalent:

Direct call to createElement:

createElement.tsx
1import { createElement } from 'react';
2
3function Greeting({ name }) {
4 return createElement(
5 'h1', // type of element
6 { className: 'greeting' }, // props
7 'Hello' // children
8 );
9}

The first parameter of the createElement is the type of the element to be rendered, then it's props and children.

Using JSX syntax:

JSX.tsx
1import { createElement } from 'react';
2
3function Greeting({ name }) {
4 return <h1 className="greeting">Hello</h1>
5}

JSX allows us to use much more readable syntax but as any abstraction hides some underlying concepts under the "magic" blanket. This use of createElement is why we cannot have conditional logic inside JSX but it also shows how React can understand what we want to create and how to render it.

Now where did I leave the keys?

The key allows React to know which components are stable and should not be removed from the DOM and re-created a new.

In our example above the list without the keys would re-render every element every time ex. when we added new item to the beginning of the list. With key added to list items it would render only the added keys and not touch the rest.

What if we wanted to list all the properties of the book?

1return (
2 <ul>
3 {/*create a listing of all the books..*/}
4 {response.map((book) => (
5 // for each book display it's title
6 <li key={book.title}>
7 {book.title}
8 <ul>
9 {/*for each property of the book display it's value by destructuring it into `key` and `value` pairs*/}
10 {Object.entries(book).map(([key, value]) => {
11 if (key === "title") return; // if the property being mapped over is the title skip it
12 return (
13 <li key={key}>
14 {key}: {value}
15 </li>
16 );
17 })}
18 </ul>
19 </li>
20 ))}
21 </ul>
22 );

Notice what we return as the key inside the li element. It's the key of the book property (title, author publishedOn etc.).

Keen eyed will shout: "You said keys needed to be unique.". That's right. Inside a loop key between siblings of that list MUST be unique. Siblings is the key word here (pun intended). This is a very important point. You can repeat keys on the page but not within the same loop. See rules of keys .

Read the documentation about why React uses keys: in React docs

Forcing a re-render

Changing the key of the component will force it to re-render itself. This can be useful sometimes when you MUST re-render a component but do not have control over it.

Rendering a fragment with siblings list with a key

When rendering the list of sibling elements when there is not one element that can be assigned a key we are not forced to close them in another div or span. In such case React will also warn us that it's an error (remember JSX vs createElement?) and it will not render the list without a single element encapsulating it inside a fragment like so: <>{children}</>.

For example:

error.tsx
1return (
2 {list.map((element) => (
3 <div>Property 1</div>
4 <div>Property 2</div>
5 <div>Property 3</div>
6 <div>Property 4</div>
7 ))}
8 );

To render such structure we need to move it inside a React fragment, it's a special structure that allows us to render elements without creating another DOM node.

error.tsx
1return (
2 {list.map((element) => (
3 <>
4 <div>Property 1</div>
5 <div>Property 2</div>
6 <div>Property 3</div>
7 <div>Property 4</div>
8 </>
9 ))}
10 );

Now we have the same problem as before, we need a key but fragments are not allowed to have keys... Unless..

unless they are used as "full" components not shorthand of <></>

FragmentedList.tsx
1return (
2 {list.map((element) => (
3 <Fragment key={element.id}>
4 <div>Property 1</div>
5 <div>Property 2</div>
6 <div>Property 3</div>
7 <div>Property 4</div>
8 </Fragment>
9 ))}
10 );

Example with components, memo and keys

Ok so this might be just enough to show exactly why key is important and how it affects rendering process in React.

You will notice that now response is outside of our component (as it should be), that Book component was extracted and it is memoized with React.memo so if the component does not change it's props or key React will skip rendering them even when state in the parent component changes. For our example we'll assume that these components are costly to recreate and we need to memoize them. By the way these are 1000+ pages books - let's assume they are heavy to carry even for React 😉.

If you want to play with it here's the link to CodeSandbox.

Styled example is just below, functionally they are the same.

Notice how removing key from a Book component forces React to re-render books after filtering even though it has not changed it's props or state. React just doesn't know that it's the same element without a key.

App.tsx
1import React, { ChangeEvent, useState } from 'react';
2
3const response = [
4 {
5 title: 'The Way of the Kings',
6 order: 1,
7 author: 'Brandon Sanderson',
8 series: 'Stormlight Archive',
9 words: '383389',
10 publishedOn: '2010.08.31',
11 },
12 {
13 title: 'Words of Radiance',
14 order: 2,
15 author: 'Brandon Sanderson',
16 series: 'Stormlight Archive',
17 words: '399431',
18 publishedOn: '2014.03.04',
19 },
20 {
21 title: 'Oathbringer',
22 order: 3,
23 author: 'Brandon Sanderson',
24 series: 'Stormlight Archive',
25 words: '451912',
26 publishedOn: '2017.11.14',
27 },
28 {
29 title: 'Rythm of War',
30 order: 4,
31 author: 'Brandon Sanderson',
32 series: 'Stormlight Archive',
33 words: '455891',
34 publishedOn: '2020.11.17',
35 },
36 {
37 title: 'Knights of Wind and Truth',
38 order: 5,
39 author: 'Brandon Sanderson',
40 series: 'Stormlight Archive',
41 words: '400000',
42 publishedOn: '',
43 },
44 {
45 title: 'Mistborn: The Final Empire',
46 order: 1,
47 author: 'Brandon Sanderson',
48 series: 'Mistborn',
49 words: '214000',
50 publishedOn: '2006-07-17',
51 },
52];
53
54// we need named function here as per eslint rules for debugging purposes
55const Book = React.memo(function Book({
56 book,
57}: {
58 book: (typeof response)[0];
59}) {
60 console.log('Rendering book: ', book.title);
61 return (
62 <li
63 key={book.title} // try removing this key, refresh the page and click the checkboxes once again
64 >
65 {book.title}
66 <ul>
67 {Object.entries(book).map(([key, value]) => {
68 if (key === 'title') return;
69 return (
70 <li key={key}>
71 {key}: {value}
72 </li>
73 );
74 })}
75 </ul>
76 </li>
77 );
78});
79
80const BookList = ({ items = [] }: { items: typeof response | [] }) => {
81 const [books, setBooks] = useState(items);
82 const [seriesFilter, setSeriesFilter] = useState(['Stormlight Archive']);
83
84 const handleFilterChange = (e: ChangeEvent<HTMLInputElement>) => {
85 const seriesName = e.target.value;
86
87 let newSeriesFilter = [...seriesFilter];
88 const filterIndex = newSeriesFilter.findIndex(
89 (element) => element === seriesName,
90 );
91 if (filterIndex !== -1) {
92 newSeriesFilter.splice(filterIndex, 1);
93 } else {
94 newSeriesFilter.push(seriesName);
95 }
96
97 setSeriesFilter(newSeriesFilter);
98 };
99
100 return (
101 <>
102 <input
103 type="checkbox"
104 id="stormlight"
105 name="series"
106 value="Stormlight Archive"
107 onChange={handleFilterChange}
108 checked={
109 seriesFilter.find((item) => item === 'Stormlight Archive') !==
110 undefined
111 ? true
112 : false
113 }
114 />
115 <label htmlFor="stormlight">Stormlight</label>
116 <input
117 type="checkbox"
118 id="mistborn"
119 name="series"
120 value="Mistborn"
121 onChange={handleFilterChange}
122 checked={
123 seriesFilter.find((item) => item === 'Mistborn') !== undefined
124 ? true
125 : false
126 }
127 />
128 <label htmlFor="mistborn">Mistborn</label>
129 <ul>
130 {books
131 .filter((book) => seriesFilter.includes(book.series))
132 .map((book) => (
133 <Book book={book} key={book.title} />
134 ))}
135 </ul>
136 </div>
137 );
138};
139
140export default function App() {
141 return (
142 <div className="App">
143 <h1>Hello Radiant</h1>
144 <BookList items={response} />
145 </div>
146 );
147}

Interactive Example

    The Way of the Kings
    The Way of the Kings
    • author: Brandon Sanderson
    • series: Stormlight Archive
    • order: 1
    • words: 383389
    • publishedOn: 2010.08.31
    Words of Radiance
    Words of Radiance
    • author: Brandon Sanderson
    • series: Stormlight Archive
    • order: 2
    • words: 399431
    • publishedOn: 2014.03.04
    Oathbringer
    Oathbringer
    • author: Brandon Sanderson
    • series: Stormlight Archive
    • order: 3
    • words: 451912
    • publishedOn: 2017.11.14
    Rythm of War
    Rythm of War
    • author: Brandon Sanderson
    • series: Stormlight Archive
    • order: 4
    • words: 455891
    • publishedOn: 2020.11.17
    Knights of Wind and Truth
    Knights of Wind and Truth
    • author: Brandon Sanderson
    • series: Stormlight Archive
    • order: 5
    • words: 400000
    • publishedOn:

Conclusion

Today we learned that:

  • 🗝️ Keys in React are used to help it understand which elements changed on the page and which have not.
  • 🗝️ Proper use of keys can lead to much more performant application by explicitly reusing DOM nodes instead of re-rendering them.
  • 🗝️ Keys must be unique within the list they are rendered.
  • 🗝️ Keys must not change between renders.

Glad you made it this far and I hope you enjoyed another adventure with me.

PS. If you need some positive strength then You should read Brandon's books even if you are not into the epic fantasy. I highly recommend them!

Back to Articles