Skip to content

usePagedList: a write issued during an in-flight load-more can shrink the loaded window #629

Closed

Description

@mforce

Found by the #511 review round (PR #628), against web/src/components/usePagedList.ts. Not a #511 defect — the hook is unmodified by that PR and this behaviour affects all seven adopting screens equally (AuditPage, ExpensesPage, FeedPage, HistoryPage, SalesPage, StockPage, WaterPage, plus #511's three new ones).

1. A write during an in-flight extension can shrink the window

runWrite claims a new ticket before the write. If the user had a load-more extension in flight when the write started, that extension is superseded and its response discarded — correct so far. But refreshWindow then re-walks only up to cursorRef.current, which is the cursor as it stood before the abandoned extension. So rows the user could see are re-read away, and the newly written record can land in the gap between the refreshed window and where the user had actually paged to.

Every test across the hook and its adopters interleaves an entity/filter switch with a replacement in flight. None interleaves a write with an extension in flight, which is why this is unpinned.

2. A one-render window before reloading flips

The entity/filter identity changes synchronously in the handler, but the hook's reload runs from useEffect(() => void reload(), [reload]), which by React's contract runs after commit. So for one render the new heading can be painted beside the previous entity's rows. It is bounded to a single paint, the ticket still wins immediately afterwards, and it is identical for every existing adopter — screens that blank on reloading (#511's two ledgers) close it a render later, not synchronously. Pre-#511 InventoryPage happened to avoid it by calling setMovements([]) in the same handler as the id change.

Worth fixing only if a guaranteed zero-flicker switch is wanted; recorded so the next person does not re-derive it.

Suggested shape

For (1), refreshWindow should walk to the furthest cursor the user actually reached, not the cursor as of the superseded ticket. For (2), an optional synchronous clear on identity change, opt-in per screen.

Both need tests that interleave a write with an in-flight extension — the case the whole suite currently misses.

Related: #511, PR #628, #469 (the hook's original extraction).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:frontendReact/Vite web clientbugSomething isn't workingepic-1.5severity:p2Defect: user-visible wrong behaviour, no data loss

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions