Summary
Following up on #599 (which closed with v0.5.706 fixing state.text() reactivity by routing the 2-arg Text(initial, synth_id) form through perry_ui_text_create_with_id + text_registry):
ForEach(countState: State<number>, render: (i) => Widget) — the second half of #599's reproducer — is not part of that fix. The list does not grow or shrink when the bound count state changes. Verified with geisterhand against a minimal repro.
Reproducer (~15 lines)
import { App, VStack, Text, Button, ForEach, state } from "perry/ui";
const count = state(2);
App({
title: "ForEach Probe",
width: 400,
height: 400,
body: VStack(8, [
count.text(), // updates correctly via #599 fix
ForEach(count, (i: number) => Text("row " + i)), // does NOT grow on .set()
Button("set count to 5", () => {
count.set(5);
console.log("count is now:", count.get()); // prints "count is now: 5"
}),
]),
});
$ perry compile probe.ts -o probe --enable-geisterhand && ./probe
Drive via geisterhand:
# Tree BEFORE click — 4 widgets (count.text + 2 row Texts + 1 Button)
$ curl -s 'http://127.0.0.1:7676/widgets?tree=true' | jq 'length'
4
# Click the button. Console logs "count is now: 5" — state holder updated.
$ curl -X POST http://127.0.0.1:7676/click/<button-handle>
# Tree AFTER click — still 4 widgets. The 3 new row Texts never get added.
$ curl -s 'http://127.0.0.1:7676/widgets?tree=true' | jq 'length'
4
count.text() does update visually (proven by adding a widgetSetText of the new value to the bound Text — state.text() propagation works post-#599). The ForEach renderer is never re-invoked.
Discovery context
Hit this in our app's orders list. The headline state("Loading…") correctly updates to "4 recent orders" after await listOrders() completes (state.text() works), but ForEach(ordersCount, render) doesn't actually render the rows — the widget registry shows the same 8 callback-bearing widgets (3 static TextFields + 5 static Buttons) before and after ordersCount.set(4). Each ForEach row should add a Button (the row's "View" action) plus a few Text widgets — none appear.
Server returns the 4 orders correctly via fetch + JSON.parse (verified). The only missing piece is ForEach reactively expanding to render them.
Suggested fix shape
Mirror the #599 approach: ForEach likely needs to register a count-change handler (analogous to the new text_registry for state.text()) so when the bound State<number> fires .set(), the macOS backend invalidates the existing children, calls the render callback for each i in [0..newCount), and inserts the resulting Widgets into the host VStack/HStack.
lazyvstackUpdate(handle, newCount) already exists for LazyVStack — ForEach could share that pipeline, just always re-render rather than virtualizing.
Environment
Why we filed
#599 closed with the state.text() half landed but the ForEach(stateCount, ...) half from the same reproducer wasn't covered. With this filed separately, the macOS native client can render dynamic lists from server data (orders, customers, products) — currently those screens populate fine on the JS side and re-render their headers via state.text(), but the actual list rows never appear.
Summary
Following up on #599 (which closed with v0.5.706 fixing
state.text()reactivity by routing the 2-argText(initial, synth_id)form throughperry_ui_text_create_with_id+text_registry):ForEach(countState: State<number>, render: (i) => Widget)— the second half of #599's reproducer — is not part of that fix. The list does not grow or shrink when the bound count state changes. Verified with geisterhand against a minimal repro.Reproducer (~15 lines)
$ perry compile probe.ts -o probe --enable-geisterhand && ./probeDrive via geisterhand:
count.text()does update visually (proven by adding awidgetSetTextof the new value to the bound Text —state.text()propagation works post-#599). The ForEach renderer is never re-invoked.Discovery context
Hit this in our app's orders list. The headline
state("Loading…")correctly updates to"4 recent orders"afterawait listOrders()completes (state.text() works), butForEach(ordersCount, render)doesn't actually render the rows — the widget registry shows the same 8 callback-bearing widgets (3 static TextFields + 5 static Buttons) before and afterordersCount.set(4). Each ForEach row should add a Button (the row's "View" action) plus a few Text widgets — none appear.Server returns the 4 orders correctly via
fetch + JSON.parse(verified). The only missing piece is ForEach reactively expanding to render them.Suggested fix shape
Mirror the #599 approach: ForEach likely needs to register a count-change handler (analogous to the new
text_registryforstate.text()) so when the boundState<number>fires.set(), the macOS backend invalidates the existing children, calls the render callback for eachi in [0..newCount), and inserts the resulting Widgets into the host VStack/HStack.lazyvstackUpdate(handle, newCount)already exists forLazyVStack—ForEachcould share that pipeline, just always re-render rather than virtualizing.Environment
perry 0.5.706(the version that closed perry/ui: state.text() and ForEach(stateCount,...) don't propagate on macOS after .set() #599)--target macos, arm64--enable-geisterhandWhy we filed
#599 closed with the
state.text()half landed but theForEach(stateCount, ...)half from the same reproducer wasn't covered. With this filed separately, the macOS native client can render dynamic lists from server data (orders, customers, products) — currently those screens populate fine on the JS side and re-render their headers via state.text(), but the actual list rows never appear.