ReviewOS

stacks/stx

fix(native): `:if` is never applied — every branch renders, and `:show` on the same node works

#2074
Closed glennmichael123 opened this 9 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
9 days ago

:if is compiled into the native binding manifest and then never applied by the native runtime. Every branch renders, in every state, and nothing is ever hidden — stxHost.setVisible is not called once.

:show on the same nodes works correctly, which is what makes this a small fix and a usable workaround today.

What a screen actually renders

A wildloop screen that has shipped, at mount, before any data has arrived:

trails-loading: PRESENT
trails-error:   PRESENT
trails-empty:   PRESENT
trails-count:   PRESENT
trails-clear:   PRESENT

text in the first frame:
["Trails", "Half a million, by name, park or forest.", "Search", "Clear", "No trail matches that."]

The spinner, the error slot, the result count, a Clear button for an empty search box, and "No trail matches that." are all on screen at once. Each is behind an :if whose condition is false.

Minimal repro

<script>
const rows = state([{ id: 1, show: true }, { id: 2, show: false }])
const flag = state(false)
</script>

<View>
  <Text :if="flag()" testID="outside-if">if-false</Text>
  <Text :show="flag()" testID="outside-show">show-false</Text>
  <Text :show="!flag()" testID="outside-show-true">show-true</Text>
  <View :for="row in rows()" :key="row.id">
    <Text :if="row.show" testID="inside-if">inside-if</Text>
    <Text :show="row.show" testID="inside-show">inside-show</Text>
  </View>
</View>

Compiled with compileNativeBundle and driven through a bare bridge, the style operations that come out are:

n2  (:show false)          -> display: none
n3  (:show true)           -> display: flex
n14 (:show, row 1 = true)  -> display: flex
n18 (:show, row 2 = false) -> display: none

Four operations, all correct. For the three :if nodes: none. They are created, they stay visible, and both rows of the :for get one regardless of row.show.

Where it stops

The compiler does its half. The manifest carries the binding:

{ id: 4, bindings: [{ name: ":if", value: "row.show", kind: "if" }] }

The native runtime has no handler for it — grep '"if"' over dist/native/runtime/*.js returns nothing — while bindShow in signals.js calls stxHost.setVisible, and the native host implements setVisible properly, emitting updateNode with style.display for a materialized node and stamping baseStyle.display for one that has not been created yet. The machinery is all there; :if is not wired to it.

Not a regression

Identical output from 0.2.504 and 0.2.520 — same probe, same empty result. So this is not new breakage, it simply had not been looked at: every earlier :if in our native screens sat on a Text whose bound text was empty in the state where it should have been hidden, so an always-rendered node looked like a hidden one.

It is also not the same bug as #2067. That one was about a handler being dead on a node revealed later, and the probe that closed it measured whether handlers ran — not whether a false condition hid anything.

Why it matters beyond cosmetics

On a list screen it puts live controls on rows that must not have them. In the screen this was found on, every challenge gets Accept and Decline buttons — including the ones the viewer sent, where :if="canAnswer(row)" is false:

createNode n43 Button testID=challenge-accept  events={onPress: "respond(row, 'accept')"}  children=["Accept"]
createNode n55 Button testID=challenge-accept  events={onPress: "respond(row, 'accept')"}  children=["Accept"]

Two rows, two Accept buttons, one of which is an offer to accept your own challenge, with a working handler behind it.

The fix, and what we are doing meanwhile

Route the if binding to the same setVisible path :show already uses. If :if is meant to unmount rather than hide, that is a larger change and worth saying so — but hiding it would at least make the screens correct.

Meanwhile we are converting :if to :show in resources/native/** and adding a gate rule so a new native screen cannot use :if until this closes.

9 days ago

Fixed in 8b1a22a (fix(native): apply conditional visibility and map bindings). Native :if now calls the host visibility path on initial render and loop rows, while preserving the existing structural lifecycle behavior. Added a production-bundle regression covering the exact standalone + :for reproduction; native regression set passed 41/41, with root typecheck and lint green.

Sign in to comment on this issue.