LearnNewsExamplesServices
Frontmatter
id17401
titleComponent cells keep their first record's content when rows recycle
stateClosed
labels
bugaigrid
assigneesneo-opus-grace
createdAtAug 19, 2026, 10:57 PM
updatedAtAug 20, 2026, 1:20 PM
githubUrlhttps://github.com/neomjs/neo/issues/17401
authorneo-opus-grace
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 20, 2026, 1:20 PM

Component cells keep their first record's content when rows recycle

neo-opus-grace
neo-opus-grace commented on Aug 19, 2026, 10:57 PM

Context

Reported by the operator against apps/devindex: scrolling into new row ranges leaves the User and Impact columns showing the values from the first page. Reproduced in the GPU e2e harness on 2026-08-19.

Live latest-open sweep: latest 20 open issues checked at 20:56Z, no equivalent. A2A claim sweep over the last 12 messages: no overlapping lane.

The Problem

One row, read at three scroll positions, in the same run:

position rank total login
page 1 1 19,876,461 laciferin2024Hiro
+100 rows 105 214,342 laciferin2024Hiro
bottom 2263 37,236 laciferin2024Hiro

rank and total track the scroll exactly. login never changes. Rows 2–5 behave identically — the same five logins appear at rank 1, rank 105 and rank 2263.

The control is inside the row. Because rank and total update in the very same recycled row, this cannot be a scroll that did not happen or a store that did not page. Only some cells are stale.

The split is by column KIND, not by column. rank and total are plain/renderer columns and are correct. login is type: 'githubUser', which extends Neo.grid.column.Component. The operator also names Impact (type: 'component'); the sparkline, iconLink and countryFlag columns share the same base and should be assumed affected until measured.

Reproduction: load /apps/devindex/, read the first rows, wheel-scroll over .neo-grid-view (native scrollTop assignment is inert — the grid virtualizes by transform), read again. A diagnostic spec was used and deliberately not committed; the app is leaving this repository and its tests go with it.

The Architectural Reality

  • src/grid/column/Component.mjs#cellRenderer — produces or recycles the cell component. It opens with a short-circuit that returns the pooled component untouched when component[recordProperty] === record && component.lastRecordVersion === record.version. The reuse branch below it deletes className/module/ntype, maps undefined to null and calls component.set(...).
  • src/grid/Body.mjs — owns row pooling and the mounted range.
  • src/grid/column/GitHubUser.mjs, Sparkline.mjs, IconLink.mjs, CountryFlag.mjs, Progress.mjs — all extend the component column and inherit the path.
  • Plain and renderer columns take a different, string-producing path and are correct, which is what makes the failure invisible in any grid without a component column.

Not yet established: whether the short-circuit fires wrongly, whether cellRenderer is reached at all on recycle, or whether the row hands the wrong component instance. The measurement above locates the class; it does not name the line. That work belongs to whoever takes this.

The Fix

Determine which of the three candidates above holds before changing anything, then make component-backed cells re-bind to the row's current record on recycle.

Note that useBindings is not the lever: Component.mjs documents those bindings as evaluated exactly once at instantiation and explicitly forbids reading record data through them, so enabling it would freeze values rather than refresh them.

Test Coverage Gap

No spec at any level exercised a component column under recycling, and none resized one either — test/playwright/unit/grid/ComponentColumnResizeSync.spec.mjs was added today to close the resize half. The scroll half is still open, and it is why this reached an operator: ColumnResizeCellSync and HeaderResizeRectSync both drive plain-column grids only.

Acceptance Criteria

  • A spec scrolls a grid containing a component column past its buffer and asserts the cell content matches the row's record — failing on the current code, so it reproduces before it guards
  • The same spec asserts a plain column in the same row updates, so a future regression that freezes the whole body is not mistaken for a pass
  • Coverage names which component column types were verified; any left unverified are listed rather than assumed
  • The fix does not enable useBindings anywhere
  • The fixture is neo-owned and does not drive apps/devindex

Out of Scope

  • Column resize on component columns — measured working in the GPU harness across six columns, and now unit-covered
  • apps/devindex itself; the app is leaving this repository
  • The neomjs/devindex repository's own grid faults, which trace to it running neo.mjs@13.1.0 without either resize fix

Related

  • neomjs/neo#17289, neomjs/neo#17327 — the two resize defects, both verified only on plain-column grids
  • neomjs/neo#17375 — the DevIndex extraction that surfaced this

Origin Session ID: 44746e37-a5f9-44c4-8c9d-f664247f0e38

tobiu referenced in commit 4d89ed9 - "fix(grid): component cells must follow their record across a scroll (#17401) (#17408) on Aug 20, 2026, 1:20 PM
tobiu closed this issue on Aug 20, 2026, 1:20 PM