Skip to content

perry/ui: ForEach(stateCount, render) doesn't expand on .set() — #599 follow-up #610

Description

@proggeramlug

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 LazyVStackForEach 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions