Back to the Library

React List Item State Jumps to the Wrong Row - The key={index} Bug

7 min read
“You star the first book in the list. Then you add a new book to the top. The star doesn't stay on the book you starred - it jumps to whatever is now sitting in that same position.”

In This Post

What `key` Tells ReactWatching It HappenThe Fix: Key by the Item, Not the SlotThe Same Bug, Two More ShapesPractice These Patterns

Practice This Pattern

Blue Belt

Star Jumps to the Wrong Book

Starring the first book and then prepending a new one moves the star onto the second book - each BookRow's local starred state is tied to its position, not to the book it belongs to.

+25 KI
Enter the Dojo
Blue Belt

Count Lands on the Wrong Row

A review counter follows its row's position rather than its item - clicking Move Up swaps two items visually, but their review counts stay behind in the old positions.

+25 KI
Enter the Dojo
Blue Belt

Notes Follow the Wrong Task

Deleting the first task in a list causes its note text to reappear in what is now the first row - the note state belonged to a position, and a different task now occupies that position.

+25 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

You star the first book in a reading list - a small per-row toggle, its own local state. Then you click "Add Book," which puts a new book at the top of the list.

The star doesn't stay with the book you starred. It moves - now it's on the second book, the one that used to be first. The new book at the top is unstarred, and the book you actually starred has lost its star entirely.

You didn't touch the star. The list reordered, and the star stayed in the same position instead of following the same book.

What key Tells React

{books.map((book, index) => (
  <BookRow key={index} title={book.title} />
))}

Each BookRow keeps its own starred state internally, via its own useState. When the list re-renders - because a book was added, removed, or reordered - React has to decide, for each BookRow element in the new list, whether it's "the same BookRow" as one from the previous render (in which case its existing state carries over) or a brand new one (which starts fresh).

That decision is made using key. Two elements with the same key, in the same position in their parent's children, are treated as the same instance across renders - same component, same internal state, carried forward.

key={index} means: "the BookRow at position 0 is always key=0, the one at position 1 is always key=1," and so on - regardless of which book is actually at that position. The key describes a slot in the list, not an item in your data.

Watching It Happen

const INITIAL_BOOKS = ["Clean Code", "The Pragmatic Programmer", "Refactoring"];
 
function BookRow({ title }) {
  const [starred, setStarred] = useState(false);
  return (
    <div>
      <span>{title}</span>
      <button onClick={() => setStarred((s) => !s)}>
        {starred ? "★" : "☆"}
      </button>
    </div>
  );
}
 
export default function ReadingList() {
  const [books, setBooks] = useState(INITIAL_BOOKS);
 
  function handleAdd() {
    setBooks((prev) => ["New Book", ...prev]);
  }
 
  return (
    <div>
      <button onClick={handleAdd}>Add Book</button>
      {books.map((book, index) => (
        <BookRow key={index} title={book} />
      ))}
    </div>
  );
}

Before clicking Add Book: position 0 is "Clean Code," key=0, unstarred. You click its star. Now position 0 (key=0) is starred - "Clean Code" is starred, because "Clean Code" is position 0.

Click "Add Book." The array becomes ["New Book", "Clean Code", "The Pragmatic Programmer", "Refactoring"]. Now position 0 is "New Book," and it's still key=0. React sees key=0 in both the old render and the new one, and concludes: "same BookRow as before - carry over its state." That state includes starred: true.

So key=0, now showing "New Book," renders with starred: true - the star appears on the new book. "Clean Code" is now key=1, which is a different key than it had last time (it didn't exist as key=1 before), so React mounts it fresh, with starred: false. The star moved because the key it was attached to moved - and the key was never attached to a book in the first place.

Symptom

Star a row, then add, remove, or reorder items above it. The star (or whatever per-row local state you're tracking) appears to "shift" to a different row - specifically, to whatever item now occupies the position the starred item used to be in.

The Fix: Key by the Item, Not the Slot

const INITIAL_BOOKS = [
  { id: 1, title: "Clean Code" },
  { id: 2, title: "The Pragmatic Programmer" },
  { id: 3, title: "Refactoring" },
];
 
let nextId = 4;
 
export default function ReadingList() {
  const [books, setBooks] = useState(INITIAL_BOOKS);
 
  function handleAdd() {
    setBooks((prev) => [{ id: nextId++, title: "New Book" }, ...prev]);
  }
 
  return (
    <div>
      <button onClick={handleAdd}>Add Book</button>
      {books.map((book) => (
        <BookRow key={book.id} title={book.title} />
      ))}
    </div>
  );
}

Each book now carries a stable id, assigned once when it's created, that never changes regardless of where the book sits in the array. key={book.id} means React's "is this the same instance" question is now answered in terms of which book this is, not which slot it's in.

Star "Clean Code" (id: 1). Click "Add Book" - a new book with id: 4 appears at the top. React sees key=4 for the first time (new instance, starts unstarred) and key=1 still present (same instance as before, starred: true carries over) - just now rendered in position 1 instead of position 0. The star stays on "Clean Code," because the key stayed on "Clean Code."

The Sensei's Hint

The fix is always the same shape: give each item a unique, stable identifier when it's created - a database id, a counter, crypto. randomUUID(), anything that won't change for the lifetime of that item - and use that as the key, instead of the array index.

The Same Bug, Two More Shapes

Reordering. "Count Lands on the Wrong Row" is the same mechanism with a "Move Up" button instead of "Add Book." Each row tracks how many times it's been reviewed. Swap two adjacent rows with key={index}, and the positions swap visually - but the review counts don't travel with the items, because key=0 and key=1 are still attached to positions 0 and 1, just now showing different items than before. The fix is identical: key={item.id} instead of key={index}.

Deletion. "Notes Follow the Wrong Task" runs the same idea in reverse. Each task has its own note input, tracked locally. Delete the first task, and the array shortens - what used to be the second task is now at index 0. With key={index}, React sees key=0 persisted across the delete and carries over its state - the first task's note - onto the row now showing the second task's label. The note didn't move with the task it belonged to; it stayed attached to the slot.

In every case, the question is the same: "after this list change, does key={index} still point at the same logical item it pointed at before?" Prepend, reorder, and delete all answer "no" for at least one row - and key={item.id} answers "yes" for every row, every time, because the key travels with the data instead of the position.

Constraints

key={index} is fine - genuinely fine, not a code smell - for lists that are rendered once and never reordered, filtered, or have items added or removed from anywhere but the end. The moment a list can change shape in the middle or at the start, and any item has its own local state (or uncontrolled DOM state like focus or input values), key={index} will eventually misattribute that state to the wrong item.

Practice These Patterns

"Star Jumps to the Wrong Book" is the prepend case - the most visually obvious of the three, since the star visibly relocates to a brand new item. "Count Lands on the Wrong Row" swaps the order of existing items rather than adding one, which is a useful second angle: nothing is created or destroyed, yet state still ends up on the wrong row. "Notes Follow the Wrong Task" closes the loop with deletion, which is often the hardest of the three to predict by eye - work through it last, tracing exactly which key value each row carries before and after the delete.