The key to “key” in React

The key to “key” in React

Every React developer in their journey has come up on a documentation or a tutorial stating the significance of “key” in rendering list items.

Let’s talk a little bit about what a key is and why is it necessary.

“Key” is a special string attribute in React elements that identifies an element and its state (changed, updated, deleted).
Most commonly key are used in rendering lists. Let’s take a functional component as an example and render a list of items.

function App() {
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
return (
<ul>
{items?.map(item => <li>{item}</li>)}
</ul>
)
}
export default App

This gives us the desired output of 10 numbers in an unordered list.
However, this also gives us an error in the console stating

Warning: Each child in a list should have a unique “key” prop

Error due to the absence of keys in the elements. To resolve the error, we need to introduce keys inside the element as follows:

  function App() {
  const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  return (
  <ul>
  {items?.map(item => <li key={item}>{item}</li>)}
  </ul>
  )
  }
  export default App
  • Sometimes we can find codes that have indexes as keys. This is highly discouraged as, if the elements get reordered, the indexes being set as the key will cause confusion.

  • Using a component: If we’re returning a component from the iterator, then the key should be present in that component and not in the child component instead.

      type Props = {
      item: number
      }
    
      function ListItem({ item }: Props) {
      return (
      <li>{item}</li>
      )
      }
    
      function App() {
      const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      return (
      <ul>
      {items?.map(item => <ListItem item={item} key={item} />)}
      </ul>
      )
      }
      export default App
    

    So far, everything works as expected and is common knowledge amongst most react developers.

    The significance of keys come up when we are dealing with state changes across different components.
    Let us consider an example of a chat application, similar to the web whatsapp interface, where the left sidebar consists of the names of contacts and the right window consists of the chat messages and input boxes. The following code is written with Vite , React, typescript and tailwind css

      import { useState } from "react";
    
      type Props = {
      selectedContact: {
      name: string;
      chats: {
      sender: boolean;
      message: string;
      }[];
      };};
    
      function ChatWindow({ selectedContact }: Props) {
      return (
      <>
      <h2 className="font-bold">{selectedContact?.name}</h2>
      <section className="h-2/3 flex flex-col">
      {selectedContact?.chats?.map((chat) => (
      <div className={"text-sm bg-slate-100 mt-4 " + (!chat?.sender ? "text-indigo-500 self-end" : "")}>{chat.message}</div>
      ))}
      </section>
      <form className="flex">
      <input type="text" className="rounded px-2 text-sm" placeholder="Enter message" />
      <button className="inline-flex text-white bg-indigo-500 border-0 px-6 focus:outline-none hover:bg-indigo-600 rounded text-xs mx-2">
      Send
      </button>
      </form>
      </>
      );
      }
    
      function App() {
      const [selectedContact, setselectedContact] = useState(0);
      const data = [
      {
      name: "John Doe",
      chats: [
      {
      sender: true,
      message: "Hello",
      },
      {
      sender: false,
      message: "Hello John",
      }, {
      sender: true,
      message: "How's the component working",
      },
      {
      sender: false,
      message: "Great so far",
      },
      ],
      },
      {
      name: "Jane Doe",
      chats: [
      {
      sender: true,
      message: "Hi",
      },
      {
      sender: false,
      message: "Hi Jane",
      },
      ],
      },
      ];
      return (
      <section className="w-full flex items-center justify-center px-3 h-screen">
      <section className="w-1/3 border-white px-2">
      <ul className="list-none">
      {data?.map((contact, idx) => (
      <li
      key={contact?.name}
      className={
      "cursor-pointer text-center rounded duration-100 " + (idx === selectedContact && "bg-white text-black")
      }
      onClick={() => setselectedContact(idx)}
      >
      {contact.name}
      </li>
      ))}
      </ul>
      </section>
      <section className="w-2/3 flex flex-col h-full justify-around">
      <ChatWindow selectedContact={data[selectedContact]} />
      </section>
      </section>
      );
      }
      export default App;
    

    The output looks something like this:

    Note: When we change the component, the text written in the input remains the same, even though the entire props change and all other content changes.

    But why is this so? React looks for changes in the components. While other components are rendered, some components such as the input doesn’t register changes even though props are changed. This denotes that when props change, a new component isn’t rendered, even though we intend it to.

    The easiest way to mitigate this is by using unique keys to the input fields.

      import { useState } from "react";
      type Props = {
      selectedContact: {
      name: string;
      chats: {
      sender: boolean;
      message: string;
      }[];
      };
      };
    
      function ChatWindow({ selectedContact }: Props) {
      return (
      <>
      <h2 className="font-bold">{selectedContact?.name}</h2>
      <section className="h-2/3 flex flex-col">
      {selectedContact?.chats?.map((chat) => (
      <div className={"text-sm bg-slate-100 mt-4 " + (!chat?.sender ? "text-indigo-500 self-end" : "")}>{chat.message}</div>
      ))}
      </section>
      <form className="flex">
      <input type="text" className="rounded px-2 text-sm" placeholder="Enter message" key={selectedContact?.name} />
      <button className="inline-flex text-white bg-indigo-500 border-0 px-6 focus:outline-none hover:bg-indigo-600 rounded text-xs mx-2">
      Send
      </button>
      </form>
      </>
      );
      }
    
      function App() {
      const [selectedContact, setselectedContact] = useState(0);
      const data = [
      {
      name: "John Doe",
      chats: [
      {
      sender: true,
      message: "Hello",
      },
      {
      sender: false,
      message: "Hello John",
      }, {
      sender: true,
      message: "How's the component working",
      },
      {
      sender: false,
      message: "Great so far",
      },
      ],
      },
      {
      name: "Jane Doe",
      chats: [
      {
      sender: true,
      message: "Hi",
      },
      {
      sender: false,
      message: "Hi Jane",
      },
      ],
      },
      ];
      return (
      <section className="w-full flex items-center justify-center px-3 h-screen">
      <section className="w-1/3 border-white px-2">
      <ul className="list-none">
      {data?.map((contact, idx) => (
      <li
      key={contact?.name}
      className={
      "cursor-pointer text-center rounded duration-100 " + (idx === selectedContact && "bg-white text-black")
      }
      onClick={() => setselectedContact(idx)}
      >
      {contact.name}
      </li>
      ))}
      </ul>
      </section>
      <section className="w-2/3 flex flex-col h-full justify-around">
      <ChatWindow selectedContact={data[selectedContact]} />
      </section>
      </section>
      );
      }
      export default App;
    

    The usage of “key” in the components ensure that React knows a new element is being rendered and not the same one as before.

    Thus explained a proper usage of keys in React outside the render of list items.

    Conclusion

    Use of “key” in React is for letting it know the difference between similar/ reusable elements in the code. Primarily used while rendering list items, it’s function remains the same — to bring distinction between the elements.

    While using elements that change its data on a state, usage of keys ensure that React does not identify them as same and persist unwanted data amongst them.