Reactbeginner

Key Prop: Why It Matters

Understand React's key prop: why it exists, how it affects rendering, when to use IDs vs indexes, and how keys control component identity and state preservation.

10 min readยทPublished Mar 22, 2026
reactkeyslistsrendering

What Are Keys?

Keys are special string attributes you provide when rendering lists of elements. They tell React which item in a list corresponds to which item in the previous render.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Without keys, React does not know if an item moved, was added, or was removed. It falls back to comparing by position, which leads to bugs and wasted work.

How React Reconciliation Works

When state changes and React re-renders, it builds a new virtual DOM tree and compares it to the previous one. This process is called reconciliation. For lists, React needs a way to match elements between the old and new trees.

Old list:          New list (item B removed):
+---------+        +---------+
| key="A" |        | key="A" |  <-- matched, reuse DOM node
+---------+        +---------+
| key="B" |        | key="C" |  <-- matched, reuse DOM node
+---------+        +---------+
| key="C" |        | key="D" |  <-- matched, reuse DOM node
+---------+        +---------+
| key="D" |
+---------+

Result: React removes B's DOM node, keeps A, C, D unchanged.
        Minimum DOM operations.

Without keys (or with index keys on a reorderable list):

Old list:          New list (item B removed):
+---------+        +---------+
| index=0 |  "A"   | index=0 |  "A"   <-- match, same content, OK
+---------+        +---------+
| index=1 |  "B"   | index=1 |  "C"   <-- match, content differs, UPDATE DOM
+---------+        +---------+
| index=2 |  "C"   | index=2 |  "D"   <-- match, content differs, UPDATE DOM
+---------+        +---------+
| index=3 |  "D"                       <-- no match, REMOVE DOM node
+---------+

Result: React updates index 1 and 2 content, removes index 3.
        More DOM operations, and component state gets mixed up.

The Bug: Lists Without Stable Keys

Here is a concrete example of what goes wrong when using index keys on a dynamic list.

import { useState } from 'react';

function InputItem({ defaultValue }) {
  // Each item has its own local state (the input value)
  const [value, setValue] = useState(defaultValue);
  return (
    <input value={value} onChange={e => setValue(e.target.value)} />
  );
}

function BrokenList() {
  const [items, setItems] = useState(['Apple', 'Banana', 'Cherry']);

  const removeFirst = () => {
    setItems(prev => prev.slice(1));
  };

  return (
    <div>
      <button onClick={removeFirst}>Remove first</button>
      {items.map((item, index) => (
        // BUG: using index as key
        <InputItem key={index} defaultValue={item} />
      ))}
    </div>
  );
}

What happens when you click "Remove first":

  1. Before: ["Apple", "Banana", "Cherry"] with keys [0, 1, 2]
  2. After: ["Banana", "Cherry"] with keys [0, 1]
  3. React sees key 0 still exists โ€” reuses the component instance that had "Apple"
  4. React sees key 1 still exists โ€” reuses the component instance that had "Banana"
  5. React sees key 2 is gone โ€” unmounts that instance
  6. Result: The first input still shows "Apple" even though it should show "Banana"

The fix is to use stable, unique identifiers:

function FixedList() {
  const [items, setItems] = useState([
    { id: 1, text: 'Apple' },
    { id: 2, text: 'Banana' },
    { id: 3, text: 'Cherry' },
  ]);

  const removeFirst = () => {
    setItems(prev => prev.slice(1));
  };

  return (
    <div>
      <button onClick={removeFirst}>Remove first</button>
      {items.map(item => (
        <InputItem key={item.id} defaultValue={item.text} />
      ))}
    </div>
  );
}

Now removing "Apple" (id: 1) means key 1 disappears. Keys 2 and 3 still match their previous instances, so "Banana" and "Cherry" keep their correct state.

Key Strategies: ID vs Index

Use Unique IDs (Preferred)

// From a database
{users.map(user => <UserCard key={user.id} user={user} />)}

// Generated with crypto
{items.map(item => <Item key={item.uuid} data={item} />)}

// From the data itself (if guaranteed unique)
{countries.map(country => <Option key={country.code} value={country.name} />)}

When Index Keys Are Safe

Index keys are acceptable only when all three conditions are true:

  1. The list is static (never reordered, filtered, or sorted)
  2. Items have no local state or uncontrolled inputs
  3. Items are never added or removed from the middle
// SAFE: static navigation links, no state, no reordering
const navLinks = ['Home', 'About', 'Contact'];

function Nav() {
  return (
    <nav>
      {navLinks.map((link, index) => (
        <a key={index} href={`/${link.toLowerCase()}`}>{link}</a>
      ))}
    </nav>
  );
}

Comparison Table

Scenariokey=key=
Items can be reorderedCorrectBuggy
Items can be removed from middleCorrectBuggy
Items have local state (inputs)CorrectState mismatch
Static, read-only listCorrect (overkill but safe)Safe
Items added to beginningCorrectUnnecessary re-renders
Performance on large listsOptimal diffingExtra DOM operations

Keys and Component Identity

Keys do more than optimize lists. They control component identity. When a key changes, React treats it as a completely different component โ€” it unmounts the old one and mounts a new one.

Resetting State with Keys

function ProfilePage({ userId }) {
  // BUG: If userId changes, the old comment draft persists
  return (
    <div>
      <h1>User {userId}</h1>
      <CommentForm />
    </div>
  );
}

// FIX: key forces remount, resetting all state inside CommentForm
function ProfilePage({ userId }) {
  return (
    <div>
      <h1>User {userId}</h1>
      <CommentForm key={userId} />
    </div>
  );
}

This is a deliberate technique, not a hack. React documents this as the recommended way to reset a component's state in response to a prop change.

userId changes from 1 to 2:

Without key:
  CommentForm instance stays mounted
  State persists (stale draft from user 1 visible for user 2)

With key={userId}:
  CommentForm (key=1) unmounts -> state destroyed
  CommentForm (key=2) mounts   -> fresh state

Forcing Re-initialization

function Timer({ duration }) {
  const [timeLeft, setTimeLeft] = useState(duration);

  useEffect(() => {
    const id = setInterval(() => {
      setTimeLeft(t => {
        if (t <= 0) { clearInterval(id); return 0; }
        return t - 1;
      });
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{timeLeft}s remaining</p>;
}

// Usage: key resets the timer completely when duration changes
function Game({ round }) {
  return <Timer key={round} duration={30} />;
}

Keys and Animations

Keys affect CSS transition and animation libraries. When a key changes, the old element unmounts and a new one mounts โ€” giving animation libraries the enter/exit signals they need.

import { AnimatePresence, motion } from 'framer-motion';

function Slideshow({ images, currentIndex }) {
  return (
    <AnimatePresence mode="wait">
      <motion.img
        key={images[currentIndex].id}   // key change triggers exit + enter
        src={images[currentIndex].src}
        initial={{ opacity: 0, x: 100 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: -100 }}
      />
    </AnimatePresence>
  );
}

Without the key, AnimatePresence cannot detect that the element changed โ€” it sees the same motion.img component and just updates props, skipping the exit animation entirely.

List Animations

function AnimatedList({ items }) {
  return (
    <AnimatePresence>
      {items.map(item => (
        <motion.div
          key={item.id}   // stable keys let AnimatePresence track items
          initial={{ opacity: 0, height: 0 }}
          animate={{ opacity: 1, height: 'auto' }}
          exit={{ opacity: 0, height: 0 }}
          layout            // animate position changes on reorder
        >
          {item.text}
        </motion.div>
      ))}
    </AnimatePresence>
  );
}

Performance Implications

Keys directly affect React's reconciliation efficiency. Poor key choices cause unnecessary DOM mutations.

Benchmark: Adding to Beginning of List

// Scenario: prepend new item to a list of 1000 items

// key={index}: React sees that EVERY key has different content
// Result: updates 1000 DOM text nodes (O(n) mutations)

// key={item.id}: React sees 999 existing keys unchanged, 1 new key
// Result: creates 1 new DOM node, inserts it (O(1) mutations)

Benchmark: Removing From Middle

// Scenario: remove item at position 500 from a list of 1000

// key={index}: items 500-999 all shift up by one position
// Result: updates 500 DOM nodes (O(n) mutations)

// key={item.id}: only the removed key disappears
// Result: removes 1 DOM node (O(1) mutations)

For small lists (under 50 items), the performance difference is negligible. For large lists, proper keys can mean the difference between a smooth 60fps interaction and visible jank.

Common Mistakes

Using Random Values as Keys

// TERRIBLE: new key every render, every item remounts every time
{items.map(item => (
  <Item key={Math.random()} data={item} />
))}

This is worse than no key at all. Every render produces new keys, so React unmounts and remounts every component โ€” destroying state, losing focus, killing performance, and breaking animations.

Using Non-Unique Keys

// BUG: if two items share the same name, React treats them as the same element
{items.map(item => (
  <Item key={item.name} data={item} />  // names might not be unique
))}

React warns in the console when it detects duplicate keys. Always use values you know are unique: database IDs, UUIDs, or guaranteed-unique natural keys.

Keys on Fragments

// Keys on Fragments use the explicit <Fragment> syntax
import { Fragment } from 'react';

{groups.map(group => (
  <Fragment key={group.id}>
    <h2>{group.title}</h2>
    <p>{group.description}</p>
  </Fragment>
))}

// Short syntax <> does not support keys
// This is a syntax error:
// {groups.map(group => (
//   <> key={group.id}>  // WRONG
// ))}

Generating Stable Keys

When your data does not have natural IDs, generate them at creation time โ€” never at render time.

import { useRef, useCallback } from 'react';

function useDynamicList(initialItems = []) {
  const nextId = useRef(0);

  const addItem = useCallback((text) => {
    return { id: nextId.current++, text };
  }, []);

  // Initialize with stable IDs
  const [items, setItems] = useState(() =>
    initialItems.map(text => ({ id: nextId.current++, text }))
  );

  const add = useCallback((text) => {
    setItems(prev => [...prev, addItem(text)]);
  }, [addItem]);

  const remove = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);

  return { items, add, remove };
}

Usage:

function TodoApp() {
  const { items, add, remove } = useDynamicList(['Buy milk', 'Walk dog']);
  const [text, setText] = useState('');

  const handleAdd = () => {
    if (text.trim()) {
      add(text.trim());
      setText('');
    }
  };

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={handleAdd}>Add</button>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.text}</span>
          <button onClick={() => remove(item.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

Quick Reference

ALWAYS use:
  - Database IDs:        key={user.id}
  - UUIDs:               key={item.uuid}
  - Unique natural keys: key={country.code}

SOMETIMES OK:
  - Index keys on STATIC, STATELESS, NEVER-REORDERED lists: key={index}

NEVER use:
  - Math.random()
  - Date.now()
  - Any value generated during render

Key Takeaways

  • Keys tell React which list items correspond between renders
  • Without stable keys, React falls back to positional comparison โ€” causing state bugs and unnecessary DOM updates
  • Use unique, stable identifiers (database IDs, UUIDs) as keys
  • Index keys are safe only for static, stateless lists that never reorder
  • Changing a key forces React to unmount and remount โ€” useful for resetting state
  • Keys enable animation libraries to detect enter/exit transitions
  • Never generate keys during render (Math.random, Date.now)
  • For large lists, proper keys are a meaningful performance optimization
  • Keys on <Fragment> require the explicit import syntax, not <>

Found this helpful?

Support devsofus โ€” help us keep creating free dev guides.

Related Articles