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.
| pin | Name | Role | Team |
|---|---|---|---|
| Ada Berglund | Platform Engineer | Graph | |
| Bo Castellanos | Principal Engineer | Core | |
| Cedar Nakamura | Frontend Engineer | Editor | |
| Devi Okonkwo | Engineering Manager | Graph | |
| Eero Virtanen | Infrastructure Lead | Core | |
| Fern Oloyede | Staff Engineer | Editor | |
| Guo Xiaomei | Senior Engineer | Data | |
| Haruto Sato | Frontend Engineer | Editor | |
| Inga Holmberg | Security Engineer | Core | |
| Jaden Ogunyemi | Senior Engineer | Graph | |
| Kai Halvorsen | Platform Engineer | Data | |
| Luz Bautista | Engineering Manager | Editor |
row.selected = true — lives on the row objectA refresh replaces the row objects. No carry-through.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.