: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: noneFour 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.