ReviewOS

stacks/stx

fix(stx): a signal in an interpolation renders [object Object], or nothing, depending on how it is written

#2069
Closed glennmichael123 opened this 9 days ago ·
9 days ago

The gap

A signal inside an interpolation has three behaviours depending on how it is written, and two of them are wrong silently.

On stx 0.2.448, with const query = state('') in a <script client> block:

WrittenRenders
[object Object][object Object] — the signal, stringified
for [object Object][object Object] — same, inside the sentence
nothing at all — the interpolation is empty
the value ✅
the value ✅

So the natural way to write a sentence — concatenating a label and a value — produces either a diagnostic string shown to the user, or silence. Both render; neither warns.

What it looked like in production

resources/views/athletes.stx carried this for an unknown length of time:

<span :else>{{ athletes.length === 1 ? 'One match' : athletes.length + ' matches' }}{{ ' for “' + query + '”' }}</span>

The heading on the athlete directory read:

One match for “[object Object]”

Note the first interpolation in that same line is correct: athletes.length is a property access on a signal and is unwrapped properly. So within one line, property access works, a bare reference stringifies the object, and a call inside a concatenation evaporates.

The fix was to build the string where the signals are and interpolate the result:

const matchLine = derived(() => {
  const count = athletes().length
  return `${count === 1 ? 'One match' : `${count} matches`} for “${query().trim()}”`
})
<span :else>{{ matchLine() }}</span>

Why this is worth a fix rather than a convention

The three behaviours are not distinguishable by reading the template, and two of them are indistinguishable from "the data is missing". [object Object] at least shows up in a screenshot; the empty case does not, so a sentence quietly loses its subject and the page looks fine to everyone who does not know what it should say.

The console says nothing in either case. tsc cannot see inside .stx. The linter is silent.

What would be enough, in order of preference

  1. Unwrap a signal in an interpolation wherever it appears, including inside an expression, the way property access already does. for [object Object] would then read the value.
  2. Evaluate calls inside a concatenation, so the explicit form is not worse than the implicit one. The empty result is the more surprising of the two failures, because it is the form a reader would reach for after seeing [object Object].
  3. Failing both: warn. A compiler diagnostic for a signal identifier used inside a larger interpolation expression would be enough, since the correct shape — build it in a derived() — is mechanical from there.

Reproducing

Any view with a state('') in a client block and the four forms in the table above, loaded in a browser. wildloop's resources/views/athletes.stx on main carries the fixed version, and CLAUDE.md now documents the rule for the next person.

Filed from wildloop's native promotion work for stacksjs/stx#1992, alongside stacksjs/stx#2067 and stacksjs/stx#2068 — three findings from the same fortnight, all of which render or compile cleanly while being wrong.

9 days ago

Fixed on main and covered by regression test c51e5d2. The current signal runtime correctly renders bare signals, called signals, and called signals inside concatenation; updates also propagate after set(). Verified by interpolated-signal.test.ts as part of 46 focused tests (0 failures).

Sign in to comment on this issue.