React path · The model · Topic 3 of 12

Reconciliation and keys

Not started

After a render, React has two trees: what is on screen and what your components just returned. Reconciliation is how it matches them up. Comparing two arbitrary trees perfectly is expensive, so React uses simple rules: elements are matched by their position among their siblings and by their type, and in lists by their key. A match keeps the component, its state and its DOM node; anything else is torn down and built again.

Why it matters: Whether a component keeps its state, and whether a DOM node is reused or rebuilt, is decided by its type and position in the tree, and by its key. Wrong keys cause the strangest bugs in React.

  1. 1Same type, same place, same component

    If an element has the same type at the same position as last render, React keeps the component instance, its state and its DOM node, and only updates the props.

  2. 2Different type, start over

    A different type at a position (a `div` becoming a `section`, or `<A>` becoming `<B>`) unmounts the old subtree with all its state and mounts a fresh one.

  3. 3Keys identify list items

    In a list, the key, not the index, says which item is which. Stable keys from the data let React move items with their state; index keys attach state to positions.

1

How it works

The explanation, with examples you can run.

Type and position

import { useState } from "react" function Field({ label }: { label: string }) {  const [text, setText] = useState("")  return (    <label>      {label} <input value={text} onChange={(e) => setText(e.target.value)} />    </label>  )} export function Form({ business }: { business: boolean }) {  return business ? <Field label="Company" /> : <Field label="Name" />}

Toggling business keeps whatever was typed: both branches put a Field at the same position, so React sees one component whose label prop changed. React tracks positions in the tree, not where the JSX was written in your file.

Change one branch to wrap the field in a <div>, or to render a different component, and the types at that position differ. React unmounts the old subtree (its state, its DOM nodes, its effects' cleanups run) and mounts a new one. This is also why you should never define a component inside another component's body: each render creates a new function, so the type is different every time and its state resets on every render.

Keys in lists

A list is a set of siblings, and positions shift when items are added, removed or reordered. A key tells React which item is which:

type Todo = { id: string; text: string } function TodoList({ todos }: { todos: Todo[] }) {  return (    <ul>      {todos.map((todo) => (        <li key={todo.id}>          <input type="checkbox" /> {todo.text}        </li>      ))}    </ul>  )}

With key={todo.id}, deleting the first todo removes exactly that li; the others keep their DOM nodes, including whether their checkbox is ticked. With key={index}, React sees keys 0, 1, 2 become 0, 1. It keeps the first two rows, gives them new text, and removes the last one. The ticked checkbox that belonged to the deleted todo now sits next to a different todo's text.

Rules for keys:

  • Use a stable id from the data. The index is only safe if the list never reorders, filters or has items inserted.
  • Keys need to be unique among siblings, not globally.
  • Never generate them during render (key={Math.random()}): every render then looks like a new list, and every item is rebuilt and loses its state.

Keys to reset on purpose

A key works outside lists too. <ContactForm key={contact.id} contact={contact} /> makes a new contact a new component, so the form's drafts and errors reset without any effect or reset logic.

What it costs

Reconciliation compares elements, not DOM nodes, so it is cheap. The expensive mistakes are the ones that make it give up: a changing type or key forces a remount, and a remount runs every component below it, recreates its DOM and loses its state.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A list of todos uses `key={index}`, and each row has an uncontrolled checkbox. You delete the first todo. What goes wrong?
  2. 2.How do you make a form forget its local state when the user switches to editing a different contact?
  3. 3.A component renders `{isAdmin ? <AdminPanel /> : <UserPanel />}` and the flag flips. What happens to `UserPanel`'s state?

0 of 3 right so far. Get all three to finish this topic.