Preserving State on Reorder
Keep local state glued to the right item when lists reorder: stable keys, position traps, and drag-and-drop gotchas.
- react
- preserving-state
You drag item C from index 2 to index 0. Every row has an expanded flag in useState. After the drag, the wrong rows look expanded. The list data reordered, but React reused fibers by position because keys were indices — or missing.
Preserving state on reorder means: state follows identity, not array index.
Docs: Rendering Lists, Preserving and Resetting State.
Minimal reproduction
function Row({ item }) {
const [open, setOpen] = useState(false);
return (
<div>
<button type="button" onClick={() => setOpen((o) => !o)}>
{item.title}
</button>
{open && <p>{item.body}</p>}
</div>
);
}
function List({ items }) {
return items.map((item, i) => (
// Broken under reorder:
<Row key={i} item={item} />
));
}
After sorting items, position 0 still has the old open state from whoever used to be first. Fix:
items.map((item) => <Row key={item.id} item={item} />)
Now the fiber for item.id moves with the item. Local open travels correctly.
Lifted selection state
If selection is lifted, store ids, not indices:
const [selectedId, setSelectedId] = useState(null);
// ...
const selected = items.find((i) => i.id === selectedId) ?? null;
Index-based selection breaks the moment the array sorts. Same for “checked” sets: Set of ids, not a boolean array aligned to positions.
Controlled reorder UIs
function SortableBoard({ cards, onReorder }) {
return (
<ul>
{cards.map((card, index) => (
<li key={card.id}>
<CardBody card={card} />
<button
type="button"
onClick={() => onReorder(move(cards, index, index - 1))}
aria-label={`Move ${card.title} up`}
>
Up
</button>
</li>
))}
</ul>
);
}
The parent owns order (array of ids or ordered entities). Children own ephemeral UI. Keys remain card.id through the entire drag lifecycle — including placeholder rows and overlay portals.
When you want state to reset on move
Rare, but real: a row’s draft should die if the row leaves a “editing zone.” Then change the key or clear state in the drop handler. Do not rely on accidental index keys — make the reset intentional.
// Force remount when lane changes
<Card key={`${card.id}-${card.laneId}`} card={card} />
Related bugs people blame on CSS
- Input caret jumps after sort → index keys.
- Video player restarts on sibling insert → unstable keys.
- Accordion open state “jumps” → state at wrong level or wrong keys.
Profile with React DevTools: watch which components remount (state loss) versus update (state kept).
Interview out-loud
“Local state is stored on the fiber. Keys tell reconciliation which fiber is which item. Stable ids preserve state across reorder; index keys leave state stuck to positions. Selection and checked sets should store ids, not indices.”
Related on this site
Further reading
Production checklist
Before you ship a change in this area, walk the list out loud:
- What is the source of truth for the data on screen?
- What happens on remount, route change, and Strict Mode double-invoke?
- Which updates are urgent (input) versus deferrable (filter large lists)?
- Did you profile before adding memo, context splits, or virtualization?
- Is there an accessibility path: keyboard, focus, names, and errors?
If you cannot answer those, the API knowledge will not save the interview or the incident.
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.