Skip to content
AtomicReps

Where your state actually lives

A lesson from React Under the Hood. Play it above, or read it through below.

  • Two branches, one position. The switch you wrote resets nothing.

    The common read: React renders the branch I wrote, so switching to the other branch of the conditional hands that component a fresh start.

    React never reads your conditional, whether it is switching a counter or the box you just predicted. It reads the tree you handed back: two return statements that both put <Counter /> first inside a div are one address, whatever the two blocks look like in your editor. Same parent, same slot, same component, so the node survives and only its props change.

    Move the branch further up or further down the function and the bug

    Takeaway: Same address, state survives: exactly what a tab wants, exactly what two different records don't.

    Run the second case and the bug is the value you were sure would be gone: a draft written for pull request 41, still sitting in the box under pull request 42, filed as "the comment box is not clearing" against a component that never had a reset in it.

  • Two branches, two positions. The switch you did not write resets both.

    The common read: Only one of these two counters is ever on screen, so they take turns in one slot and share the score between them.

    Write the switch as two && guards, one counter visible at a time: how many scores are in play?

    Flip isPlayerA and the other counter appears: it comes up

    Rendering a component from two places gives it two addresses, and two addresses is two independent pieces of state whether both are visible at once or not. Write the switch as a pair of && guards and you have not written a switch: you have written two components, each removed when the other appears.

    Takeaway: Removal is what destroys state, so a switch built from two guards resets in both directions. Nothing ever carries across.

    The docs make the same point with one element assigned to a variable and rendered twice: two counters that share nothing, because the element was never what held the state, the two slots it landed in were. That is the shape a "just extract it into a variable and render it in both branches" review comment produces, and the ticket that follows says the filter panel forgets its selection every time the sidebar opens.

  • Inside an array, the address is the key, and no key means the index.

    The common read: the two spellings return the same markup, so React is handed the same tree either way and the editor below the banner keeps what the user typed.

    Those two spellings did not hand React the same tree. Team two put the children into an array and grew it a new first entry, so the editor's address went from index 0 to index 1. Team one's editor stayed the second child of the same div on both passes and never moved.

    Omit a key on an array child and you have

    With the index standing in for the key, every edit to the array re-points the whole list, because the item that used to be entry 0 is not what the address "index 0" means anymore:

    an insert shifts every later item's default address down a delete shifts every later item's default address up a sort scrambles every default address at once

    Takeaway: Nothing about this is visible in a screenshot: the text is still in the right-looking box, just wrong about which record it belongs to.

    The report that eventually arrives says a colleague's note showed up on the wrong customer, and it goes to whoever owns the customer list rather than to whoever added the sort.

  • An index key is the position wearing a costume.

    The common read: an index is unique inside this render, so key={i} satisfies the contract, and it only goes wrong for a list I actually sort.

    What does key={i} buy you that omitting the key did not?

    Expand Bob in an index-keyed contact list, then delete Alice above him: the detail panel

    Writing key={i} buys you nothing at all: the index is what you already had. A list keyed by index is a list with no keys and a silenced warning, and the index is a name tag that reads "whoever is standing here."

    Takeaway: The failure is not limited to sorting. Deleting the first row shifts every later row down one address, and the open panel, the checked box, the half-typed reply all stay with the slot instead of the record.

    That arrives as a data-integrity bug, filed against the row component or the endpoint that served it, in a sprint whose only change was a sort control. Nobody reads the keys.

    The other end of the rule is worse, and it's what people reach for when the warning won't go away: a key minted during render, key={Math.random()} or a template string with a sort order baked in, never matches anything from the previous pass. Every row is destroyed and rebuilt on every parent render, including renders that have nothing to do with the list.

    Takeaway: Use a stable id from the data. No id on the data? Mint one when the item is created, never when it is drawn.

  • Removed is destroyed. Nothing is paused.

    The common read: conditionally not rendering a component takes it off the screen, so its state is parked somewhere and comes back the way I left it.

    Where does the score go while the counter is off screen?

    Swap the counter out for a paragraph, then swap it back: the score that read 2 now reads

    There is no parking. React does not run a coat check, it runs a demolition: a component that stops being rendered has its state destroyed at that moment, and rendering it again later mounts a new one from scratch.

    Takeaway: The same rule runs the surprising direction too: a different component type at one position resets the whole subtree beneath it, not just the node you swapped. A wrapper change three levels up empties a form nobody touched.

    This model is worth killing rather than correcting because of where it hides. It never shows up in a code review. It shows up as a user who filled in six fields, opened the help drawer that unmounts the form behind it, closed the drawer, and got six empty fields back, filed as a complaint about the drawer.

  • key is the reset, and it is one line of the diff.

    The common read: a key clears the state that went stale, so writing one on the thing that shows the stale value clears that thing and leaves everything else alone.

    The key does not belong on the thing showing the stale value. It belongs on the component that owns that value, and it takes everything filed under that component down with it. <Chat key={to.id} contact={to} /> says a chat with Alice and a chat with Bob are two different things drawn in the same place, so every piece of state under that tree resets on the switch, with no list to maintain anywhere.

    Keys look like a list feature because that's where you meet them first, but they are

    Takeaway: The key marks the top of what gets destroyed. Nothing above it or beside it moves: one node lower and you've keyed something the value was never filed under, one node higher and you take a sibling's state with it.

    The Effect version is the same intent written as a chore: it clears the variables you remembered, renders once with the stale ones first, and needs a new line in a second file every time somebody adds state below the page.

    The symptom the key version prevents gets a person in trouble rather than a service: a half-typed message, still sitting in the box after the recipient changed, sent to the wrong person by someone who assumed the box belonged to the conversation on screen.

  • A changed key re-creates the DOM, not only the state.

    The common read: the only thing a key affects is React state, so putting one on a plain img with no state in it changes nothing.

    A different key at a position is a different node, and a different node is a different DOM element: React builds a fresh one instead of writing new attributes onto the old one. That's the whole of the gallery fix.

    That's also why key is worth reaching for on elements holding no React state at all. Anything the browser keeps on the element rather than in your state goes with it:

    the decoded image it is still painting the text typed into an uncontrolled input the scroll offset inside it

    Takeaway: Re-creating is neither free nor selective. A key on a wrapper high in the tree tears down every DOM node beneath it, unmounts every component under it, and re-runs every Effect's cleanup and setup.

    That's the right trade for switching records and the wrong one for a filter that changes twice a second. The failure you'll actually meet is the second one: a performance ticket about typing lag in a search box, opened against the list, in a week nobody touched the list.

  • Pick the address, or move the value out of its reach.

    The common read: a key is the tool for resetting state, so anywhere state is surviving something it should not, the answer is to find a value to key on.

    State just survived a switch it should not have: is the fix to find a value to key on? Three tools, and the key is only one of them.