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.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.