Skip to content

fix(nanoviews): show the for_ placeholder when there is no array at all - #205

Merged
dangreen merged 1 commit into
mainfrom
fix/nanoviews-for-empty-array
Aug 20, 2026
Merged

fix(nanoviews): show the for_ placeholder when there is no array at all#205
dangreen merged 1 commit into
mainfrom
fix/nanoviews-for-empty-array

Conversation

@dangreen

Copy link
Copy Markdown
Member

for_ over a signal that holds null or undefined threw:

TypeError: Cannot read properties of null (reading 'length')

Both on the first render and on a change from a list to nothing. The static form already did the right thing — it happens to be written $items?.length ? … : else_?.() — so the two forms disagreed about the same input.

No array is the same emptiness as an empty one, so the fix is to say that:

-    const itemsCount = items.length
+    // No array at all is the same emptiness as an empty one: the rows go and
+    // the placeholder comes, and nothing below ever reaches into it
+    const itemsCount = items?.length

Nothing else in the loop needed a guard. The array is only touched further down when itemsCount is truthy, and TypeScript narrows it from that alone — two attempts to add a ! were rejected by the linter as unnecessary assertions.

The overload arm

The interesting half is the types. Widening the writable overload from WritableSignal<T[]> to WritableSignal<T[] | EmptyValue> looks like the obvious move and is wrong: a signal is invariant, so WritableSignal<Player[]> does not fit the widened parameter, every existing caller falls through to the readable overload, and its rows silently become read-only. Four existing tests failed with Expected 0 arguments, but got 1 on a row write.

So the nullable array is a separate arm of the same signature:

// A signal is invariant, so the nullable array is a separate arm rather than
// a widening of the first: `WritableSignal<T[]>` does not fit
// `WritableSignal<T[] | EmptyValue>`, and widening would take the rows of
// every existing caller down to read-only
export function for_<T>(
  $items: WritableSignal<T[]> | WritableSignal<T[] | EmptyValue>,
  track?: (item: T, index: number) => unknown
): (
  each_: WritableEach<T>,
  else_?: () => Child
) => Child

The readable and static overloads widen normally.

Tests

One test walking all four transitions: absent on first render shows the placeholder, absent → list shows the row, list → absent brings the placeholder back, and absent → [] leaves it alone rather than tearing it down and building it again. 110 pass.

Size: +1 B gzip on all publics, average usage unchanged, every pin stays where it is.

… all

A reactive `for_` read `items.length` and threw on a nullish array - both on the first render and on a change from a list to nothing. Only the static form handled it, because it happened to be written with `?.`. No array is the same emptiness as an empty one: the rows go and the placeholder comes.

The nullable array is a separate overload arm rather than a widening of the writable one. A signal is invariant, so `WritableSignal<T[]>` does not fit `WritableSignal<T[] | EmptyValue>`, and widening in place would take the rows of every existing caller down to read-only.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.32%. Comparing base (42e613b) to head (244f09a).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #205   +/-   ##
=======================================
  Coverage   85.32%   85.32%           
=======================================
  Files         139      139           
  Lines        3142     3142           
  Branches      591      591           
=======================================
  Hits         2681     2681           
  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.

@dangreen
dangreen merged commit 6e6568b into main Aug 20, 2026
10 checks passed
@dangreen
dangreen deleted the fix/nanoviews-for-empty-array branch August 20, 2026 14:26
@github-actions github-actions Bot mentioned this pull request Aug 20, 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