Skip to content

feat(nanoviews): hand the tracking key to the row - #217

Merged
dangreen merged 1 commit into
mainfrom
feat/nanoviews-row-key
Aug 25, 2026
Merged

feat(nanoviews): hand the tracking key to the row#217
dangreen merged 1 commit into
mainfrom
feat/nanoviews-row-key

Conversation

@dangreen

Copy link
Copy Markdown
Member

A row is the row its tracker named. The key is the one thing about it that cannot change — a different key is a different row — and until now the loop kept it to itself:

for_($data, trackById)(
  ($row, $index) => {
    const { $id, $label } = record($row)

    return tr({ class: () => $isSelected($id()) ? 'danger' : '' })(
      td({ class: 'col-md-1' })($id),
      

$id is a signal over a value that was known before the row existed and will never move. It costs a child signal, an effect and links, on every row, to read something the loop already had. The alternative — closing over the item the row was built from — is wrong the moment the row is reused for another item.

So the key comes third, as a plain value:

for_($data, trackById)(
  ($row, $index, id) => tr({ class: () => $isSelected(id) ? 'danger' : '' })(
    td({ class: 'col-md-1' })(id),
    

Three details:

  • as_ carries it through. as_(record, ($row, $index, key) => …) works the same.
  • The static arm hands over the index, which is the key a list without a tracker is reconciled by. That arm used to pass each_ straight to Array#map, so a third parameter received the array — a trap for anyone who ever wrote one.
  • trackById and trackBy became generic over what they return. They were typed => unknown, which would have made the key unknown and the whole thing useless.

Without a tracker the key equals the index and the two never disagree: the lookup is keyed by position then, so the row at position i is always the one whose key is i.

Measured

The row's key is only worth something to a component that takes it, so this was measured with the benchmark app rewritten to use it (a separate PR). 30 iterations per arm, two rounds with the arms alternating:

case script before script after 95% CI
01 create 1k 24.70 20.00 (−19%) [−5.05; −3.95]
02 replace all 34.95 30.50 (−13%) [−6.15; −3.60]
07 create 10k 205.05 186.25 (−9%) [−24.70; −14.05]
08 append 1k 25.15 21.30 (−15%) [−4.60; −3.15]

22_run-memory goes from 3.449 MB to 3.083 — 366 KB less for a thousand rows, with both rounds agreeing to the third decimal.

What leaves each row is the whole reactive apparatus that existed only to deliver the id: the proxy trap, a child computed, an effect and three links.

Cost

+11 B gzip (7558 → 7569). No pin moves. If #216 lands first its Average usage pin already covers the pair; whichever goes second wants its pins re-checked after the rebase.

Four tests: the key in the reactive arm and that it stays with a row across a reorder, the index as the key without a tracker, the same for a static array, and the key through as_. Lint, tsc --noEmit and 131 tests are green.

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.38%. Comparing base (5942baf) to head (4e1ca5f).

Files with missing lines Patch % Lines
packages/nanoviews/src/flow/for.ts 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #217   +/-   ##
=======================================
  Coverage   85.38%   85.38%           
=======================================
  Files         140      140           
  Lines        3154     3155    +1     
  Branches      594      594           
=======================================
+ Hits         2693     2694    +1     
  Misses        332      332           
  Partials      129      129           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

A row is identified by the key its tracker named it by, and that key is the one thing about a row that cannot change: a different key is a different row. Until now the loop kept it to itself, so a component that needed the key had to read it back out of the row reactively - paying a child signal, an effect and a few links for a value that was already known and would never move - or capture it from the item it was built with, which is wrong the moment the row is reused.

The render function now receives it third, as a plain value: `($row, $index, key) => …`. `as_` carries it through the transform, and the static array arm hands over the index, which is the key a list without a tracker is reconciled by. `trackById` and `trackBy` became generic over the value they return, so the key arrives typed instead of `unknown`.

Reading the key rather than a signal over it takes 19% off the script time of `01_run1k` and 366 KB off the memory of a thousand rows; it costs 11 bytes gzipped.
@dangreen
dangreen force-pushed the feat/nanoviews-row-key branch from d34b7af to 4e1ca5f Compare August 25, 2026 16:03
@dangreen
dangreen merged commit 34948cd into main Aug 25, 2026
9 of 10 checks passed
@dangreen
dangreen deleted the feat/nanoviews-row-key branch August 25, 2026 16:09
@github-actions github-actions Bot mentioned this pull request Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant