A colleague ships a 40-line <Table> before lunch. An <tbody>, rows.map(r => <tr>...), a selected flag set directly on each row object, and an onClick that flips the flag. Fifty rows of mock data; QA passes; merged by 3 PM.

Three weeks in, the table points at the live server. A refresh fires every fifteen seconds. Below is the same 40-line table wired to that environment — pin as many rows as you want, then tap the refresh button and watch what the structural mistake actually looks like.

Employees · directory
pinNameRoleTeam
Ada BerglundPlatform EngineerGraph
Bo CastellanosPrincipal EngineerCore
Cedar NakamuraFrontend EngineerEditor
Devi OkonkwoEngineering ManagerGraph
Eero VirtanenInfrastructure LeadCore
Fern OloyedeStaff EngineerEditor
Guo XiaomeiSenior EngineerData
Haruto SatoFrontend EngineerEditor
Inga HolmbergSecurity EngineerCore
Jaden OgunyemiSenior EngineerGraph
Kai HalvorsenPlatform EngineerData
Luz BautistaEngineering ManagerEditor
Pins right now0
Peak (this session)0
Lost on last refresh0
Flag storagerow.selected = true — lives on the row objectA refresh replaces the row objects. No carry-through.
Tip: pin 4 rows, then tap refresh. Watch “Lost on last refresh” spike.

Count the moves. You tap a row — the handler runs rows[i].selected = !rows[i].selected and pushes a new array reference so React re-renders. The pin sits on the ROW OBJECT. Now tap the refresh button. The refresh handler rebuilds the list: same ids, same names, brand-new JavaScript objects with no selected field. The table re-paints fresh.

The pins aren't “cleared” by the refresh — they never existed on the new objects in the first place. The flag rode the OLD row objects to the garbage collector. A refresh every fifteen seconds means the user loses a selection window of, at most, fourteen seconds. In practice they lose it the instant they start reading the bulk-edit panel.

— Pin a few rows, tap refresh, watch what happens. Come back when you've felt it.