Skip to content

fix(lazy): handle empty item content without breaking mount termination#42

Merged
chiefcll merged 2 commits into
mainfrom
fix/lazy-rendered-count-guards
Jul 11, 2026
Merged

fix(lazy): handle empty item content without breaking mount termination#42
chiefcll merged 2 commits into
mainfrom
fix/lazy-rendered-count-guards

Conversation

@chiefcll

@chiefcll chiefcll commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Alternative to #26, addressing the same bug plus the issues found while reviewing it. Thanks @girouxc for the diagnosis — the core insight (the mount decision must look at container.children.length, because items that render empty insert no child node) is adopted here.

The bug (#26): when items in a LazyRow/LazyColumn render empty, offset() overstates the rendered edge. selected (an index into actual children) then never gets within buffer of offset, so updateOffset early-returns forever and navigation stops mounting new items — the list dead-ends.

Why not exactly the #26 one-liner: replacing rendered with container.children.length in both halves of the guard introduces two new problems, because maxOffset (props.each.length) is in data units while children.length is in node units:

  1. Multi-node item templates permanently stall mounting. Solid's universal renderer flattens fragments into direct siblings, so an item rendering 2 top-level nodes doubles children.length. With 20 items × 2 nodes, after 10 items mount children.length (20) >= maxOffset (20) — the guard early-returns forever and items 11–20 can never mount.
  2. The termination guard goes dead in the empty-item case it fixes. children.length never reaches maxOffset when any item renders empty, so once everything is mounted the guard never short-circuits; with delay set, every keypress at the list edge clears and re-arms navDelayTimer around a no-op bump, indefinitely.

The fix: keep each comparison in its own unit system —

const rendered = container.children.length;
if (offset() >= maxOffset || selected < rendered - buffer()) return;
  • Proximity (selected < rendered - buffer()) uses child units, matching selected — this is the [bugfix] Lazy components empty content handling #26 fix.
  • Termination (offset() >= maxOffset) stays in data units, immune to the child count diverging in either direction.

Also in this PR

  • Regression tests (tests/lazy.test.tsx) — there was no lazy coverage at all. One test reproduces the [bugfix] Lazy components empty content handling #26 scenario (items rendering empty; fails on main), one pins the multi-node-template case.
  • VirtualGrid barrel-init fixVirtualGrid called lngp.withScrolling(false) at module init through the package barrel, but index.ts re-exports withScrolling after VirtualGrid, so any import with the barrel as entry (including the new tests) threw withScrolling is not a function. It now imports the module directly.

Known remaining inconsistency (out of scope)

lazyScrollToIndex still mixes units: it sets offset = index + buffer (data units) but scrollToIndex(index) indexes children. With empty items the target child may not exist — downstream guards (children[index]?.setFocus(), withScrolling's bounds check) prevent a crash, but scroll/focus silently no-op. Fixing it requires mapping child indices back to data indices, which deserves its own change.

Testing

  • npm test — 136/136 passing (11 files), including the two new lazy tests
  • New empty-content test verified to fail against the pre-fix updateOffset
  • npm run tsc and npm run lint clean

🤖 Generated with Claude Code

chiefcll and others added 2 commits July 3, 2026 20:12
VirtualGrid calls withScrolling(false) at module init through the package
barrel, but index.ts re-exports withScrolling after VirtualGrid, so when
the barrel is the import entry the binding is still uninitialized and the
call throws. Import the module directly instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ffset

Items that render empty insert no child node, so offset() overstates the
rendered edge and navigation stops mounting new items (#26). Use
container.children.length for the proximity check (child units, matching
container.selected) while keeping termination on offset() >= maxOffset
(data units) — a diverged child count must neither stop mounting early
(multi-node item templates inflate it) nor keep the delay-timer path
running forever at the end of the list (empty items deflate it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chiefcll chiefcll merged commit bd715da into main Jul 11, 2026
1 check passed
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