Files
fusion/packages
gsxdsm 7003dc9803 U12 part 6: Board re-rendered every column on every state change — one inline arrow, measured with a memo-comparator probe (#2528)
## U12 part 6 — Board re-rendered every column on every state change

**Stacks on #2525.** Merge that first.

`canDropTask` was allocated as a fresh inline arrow, per column, per
render:

```tsx
canDropTask={(taskId) => canDropTask(taskId, columnDef.id, selectedWorkflow.id)}
```

`Column` is `React.memo`, and a new function identity on any prop
defeats that entirely. So **any** Board state change — collapsing
Archived, changing Done sort, opening the workflow switcher —
re-rendered every column and every card beneath it, not just the
affected one.

Bound through a `useMemo` cache keyed by lane + column. After the fix,
collapsing Archived re-renders exactly one column: `archived`.

### Measured, not guessed

I instrumented `React.memo`'s comparator to print which props actually
change identity on a collapse toggle. For every unaffected column the
answer was exactly one:

```
PROBE todo         changed: canDropTask
PROBE in-progress  changed: canDropTask
PROBE in-review    changed: canDropTask
PROBE done         changed: canDropTask
PROBE archived     changed: canDropTask,collapsed     <- the one that should re-render
```

After:

```
PROBE archived     changed: collapsed
```

### Why this hid, and why my first attempt failed

Two things worth recording, because both were mistakes I made in this
program:

**The test was pointed at dead code.** "keeps unaffected columns stable"
measured the **legacy single-lane board**, whose props were all stable —
so it passed for a long time while covering nothing operators use.
Deleting that board in part 1 repointed it at the real board, where it
failed 3-vs-2. I skipped it then rather than weaken it to the observed
number, and said it needed its own investigation. This is that
investigation.

**My first fix was wrong and I was right to revert it.** In part 1 I
tried a `useRef` cache invalidated by `useEffect`, it did not fix the
test, and I reverted it as unproven rather than ship it. The reason is
now clear: the effect runs *after* the render that populated the cache,
so it wipes the very bindings that render created and the next render
allocates fresh ones — the invalidation defeated the cache. `useMemo`
keyed on the resolver has no such window; the map lives exactly as long
as the closure owning it.

### Revert-proof

The test is un-skipped **with the fix, not with a new expected number**.
Restore the inline arrow at either call site and it fails 3-vs-2 again.

### Verification

`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, dashboard typecheck
green. Board, Board.canDropTask, workflow-resolved-columns and
board-no-legacy-flash: 132 passed, 0 failed, **0 skipped** — the skip
introduced in part 1 is gone.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Move menus now show exactly the destinations permitted by each custom
workflow, including non-adjacent moves.
- Invalid or hidden destination columns are excluded from move options.
  - Older workflow data continues to use a compatible fallback behavior.

- **Performance**
- Improved board responsiveness by preventing unaffected columns and
cards from re-rendering when archived sections collapse or Done sorting
changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:23:07 -07:00
..
2026-07-26 18:11:47 -07:00