Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: Map rendering component causing unnecessary re-renders, child key props not working?

I keep getting a unique key child error when rendering my components. My data coming in has a UUID that I use as a key prop for each child. For every child I have, react re-renders that many times. What is causing the re-renders?

// Contact.js

import React from 'react';

const Contact = ({ user, chat, activeChat, setActiveChat }) => {

    if (!chat.id) return null;

    const lastMessage = chat.messages[chat.messages.length - 1];
    const chatSideName =
        chat.users.find(name => name !== user.username) || 'General';
    const classNames = activeChat && activeChat.id === chat.id ? 'active' : '';

    return (
        <div
            key={chat.id}              // ebd4698f-c4b2-4dfe-92b5-2f6636d98db8
            className={`user ${classNames}`}
            onClick={() => {
                setActiveChat(chat);
            }}
        >
            <div className="user-photo">{chatSideName[0].toUpperCase()}</div>
            <div className="user-info">
                <div className="name">{chatSideName}</div>
                {lastMessage && (
                    <div className="last-message">{lastMessage.message}</div>
                )}
            </div>
        </div>
    );
};

export default Contact;

This is the rendering method in the container component's render() method responsible for rendering <Contact />

// SideBar.js
{chats.map(chat => {
    return (
        <Contact
            chat={chat}                                            
            user={user}
            activeChat={activeChat}
            setActiveChat={setActiveChat}
        />
    );
})}
like image 979
kayq Avatar asked Sep 17 '25 17:09

kayq


2 Answers

A few things I would point out. Number one is what Shubham and Fawzi pointed out: The key attribute should always be set on the top-level component you are returning from your map() function and not nested inside it.

More importantly though, I think you are misunderstanding what the key attribute does (or rather doesn't do). In particular, the key attribute does not under normal circumstances prevent or reduce the number of times a component is rendered. In your case, every time your top level component re-renders, every single item in your chats list will also re-render.

So, if it's always going to re-render, what's the point of key? Well, the key to understanding this, is to understand the difference in what react calls render and what it calls reconciliation. Render is the process that just executes the render function in your code and produces the virtual dom. The virtual dom is just plain javascript objects, so while the function is called "render", it does not actually render anything to the screen.

The real magic happens in what is called reconciliation. What this means is that the react engine has to take the "rendered" virtual dom, compare it to the real dom that's in the browser, figure out what changed, and try to come up with the instructions needed to change the browser dom to match the new virtual dom.

The problem with lists is that it can get tricky to optimize these changes without knowing whether a change is due to an insert, a delete, or just an in-place edit. For example take a list of names:

["John Doe", "Jane Doe", "Alice Smith", "Bob Smith"]

Rendered as:

<ul>
     <li>John Doe</li>
     <li>Jane Doe</li>
     <li>Alice Smith</li>
     <li>Bob Smith</li>
 </ul>

And then you change the order:

["Alice Smith", "John Doe", "Bob Smith", "Jane Doe"]

It should now needs to look like:

<ul>
     <li>Alice Smith</li>
     <li>John Doe</li>
     <li>Bob Smith</li>
     <li>Jane Doe</li>
 </ul>

There are a couple of ways that the reconciler can take the first dom and turn it into the second dom. The first is to just go through each <li>, and change the text content to match the new text. This might not be horribly inefficient in this simple example, but what if each list item was a complicated big chunk of html? Dom manipulation is expensive and if all that changed was the order of the items, it might be much easier to just move the existing <li> elements around and not need to touch the inside contents.

This is where the key attribute comes into place. The key attribute helps the reconciler understand the nature of the change. Now each <li> is tied to a specific key. If the key moved locations in the virtual dom, the reconciler can now simply create a set of actions that moves it to the same place in the real <li>. This can help even more when elements in the middle of the list are deleted:

["Alice Smith", "Bob Smith", "Jane Doe"]

With the key attribute the reconciler can now go find the the exact <li> that was deleted and simply remove it from the dom. Without the key attribute, the reconciler would look at the second array element in the virtual dom, and say.... hmm... the second element used to be "John Doe", but now it is "Bob Smith", I'm going to have to go update the second <li> to now read "Bob Smith". Then it would look at the third element say... hmm... third element used to be "Bob Smith", now it's "Jane Doe" and write dom manipulation to change the text again. Imagine if you had 100 names, and you deleted the second one. It would now have to do 99 dom updates. If you have a key attribute, it would just make a single delete to the second <li>.

So to summarize, the key attribute will not prevent the render function from being called in your code. The only way to prevent this is to use pure components, or implement componentShouldUpdate(). What key attribute will do, is make the reconciliation (i.e. the dom manipulation part) go faster.

like image 103
Daniel Tabuenca Avatar answered Sep 19 '25 07:09

Daniel Tabuenca


You need to provide a key to the component you render using map and not inside it.

// SideBar.js
{chats.map((chat, index) => {
    return (
        <Contact
            key = {chat.id || index}
            chat={chat}                                            
            user={user}
            activeChat={activeChat}
            setActiveChat={setActiveChat}
        />
    );
})}

Also your Contact component will have as many instances as the number of elements in chats arrays and hence the render method console.log in Contact will be triggered as many times.

Also to further optimise your app you could use React.PureComponent or React.memo for your Contact component

like image 30
Shubham Khatri Avatar answered Sep 19 '25 08:09

Shubham Khatri