Skip to content

feat(nanoviews): add match_ for a cascade of conditions - #219

Merged
dangreen merged 1 commit into
mainfrom
feat/nanoviews-match
Aug 26, 2026
Merged

feat(nanoviews): add match_ for a cascade of conditions#219
dangreen merged 1 commit into
mainfrom
feat/nanoviews-match

Conversation

@dangreen

@dangreen dangreen commented Aug 26, 2026

Copy link
Copy Markdown
Member

A screen that waits, then fails, then has something to show is three conditions deep, and if_ nests to say it:

if_($loading)(
  () => i()('Loading'),
  () => if_($error)(
    $error => b()($error),
    () => article()($post.$title)
  )
)

The last branch reads at the bottom of a staircase built out of the two before it, and adding a fourth state moves everything.

match_ takes the same cascade as a list:

match_(
  when_($loading, () => i()('Loading')),
  when_($error, $error => b()($error)),
  when_($post, $post => article()($post.$title)),
  default_(() => 'Nothing to show')
)

The first case whose value is truthy renders; default_ answers when none do and may stand anywhere in the list.

What when_ hands the branch

The branch gets the case's signal, narrowed through TruthySignalish — the same contract if_ gives its then_. Not the value: a branch that took the value would depend on it, and every change inside would rebuild the whole subtree instead of updating the text node that changed. There is a test for exactly that — the post's title changes and the <b> node stays the same object.

One computed, and what it buys

export function match_(...cases: AnyMatchCase[]) {
  const fallback = cases.find(matchCase => matchCase[0] === default_)

  return swap_(
    computed(() => cases.find(
      matchCase => matchCase[0] !== default_ && $get(matchCase[0])
    ) || fallback),
    matched => matched?.[1](matched[0])
  )
}

The computed returns the winning case, and swap_ swaps content when that changes. The case tuple is built once, so its identity is the memo key: while the same case keeps winning, a change to its value updates the branch in place and never rebuilds it. That gate — oldValue !== (value = compute()) — is what a computed has and an effect does not, which is why the walk cannot simply live inside the swapper.

The walk stops at the case that holds, so the block reads the values above the winner and no further. A case below the one that won is never asked, and never wakes the block when it changes — laziness that falls out of the early exit rather than being arranged.

Measured

The shape was reviewed against the alternatives — an index instead of the tuple (+11 B, same mechanics), no computed at all (breaks the in-place update), nested if_ (N computeds and N swap blocks), selector (a derivative of one source, and there are N here). All heavier; the current form stands.

find over a hand-written loop costs a call per case — 5 ns per walk across 2, 4 and 8 cases — and saves 21 B gzip. The walk runs when a value changes, so the bytes win by the house rule; the trade-off is written down at the call site in case a profile ever says otherwise.

Cost: +79 B gzip (7581 → 7660), one pin moves, All publics (Gzip) 7.6 → 7.7 kB.

Tests

Four stories under Logic/match_ and eight tests written through them, the way if_ and switch_ are: the cascade with and without a default, the branch keeping its node while the value changes, a case below the winner staying unread, and the pair that documents batch — the cases moving together walk once, the same cases moving one after another walk twice and show the frame in between.

Lint, tsc --noEmit and 139 tests are green.

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.40%. Comparing base (66e9ee3) to head (0927124).

Files with missing lines Patch % Lines
packages/nanoviews/src/flow/match.stories.ts 84.21% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #219      +/-   ##
==========================================
+ Coverage   85.35%   85.40%   +0.05%     
==========================================
  Files         140      142       +2     
  Lines        3155     3180      +25     
  Branches      594      595       +1     
==========================================
+ Hits         2693     2716      +23     
- Misses        332      335       +3     
+ Partials      130      129       -1     

☔ 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 screen that waits, then fails, then has something to show is three conditions deep, and `if_` nests to say it: the third branch reads at the bottom of a staircase built out of the two before it.

`match_` takes the cascade as a list. The first case whose value is truthy renders, `default_` answers when none do, and a case is written with `when_`, which hands its own value to the branch the way `if_` does - as a signal, narrowed, so a change inside the branch updates the text and leaves the branch standing.

```
match_(
  when_($loading, () => i()('Loading')),
  when_($error, $error => b()($error)),
  when_($post, $post => article()($post.$title)),
  default_(() => 'Nothing to show')
)
```

The block reads the cases in order and stops at the one that holds, so it subscribes to the values above the winner and no further: a case below it is never asked, and never wakes the block when it changes.
@dangreen
dangreen force-pushed the feat/nanoviews-match branch from 2a84e11 to 0927124 Compare August 26, 2026 14:14
@dangreen
dangreen merged commit 1065b20 into main Aug 26, 2026
10 checks passed
@dangreen
dangreen deleted the feat/nanoviews-match branch August 26, 2026 14:25
@github-actions github-actions Bot mentioned this pull request Aug 26, 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