Build a task app¶
This complete example combines controlled forms, immutable list updates and
stable keys. Initialize an app, replace app.py, and run pyreact-forge dev.
from pyreact_forge import component, html, use_id, use_ref, use_state
@component
def App():
tasks, set_tasks = use_state([])
text, set_text = use_state("")
field_id = use_id()
next_id = use_ref(1)
def add(event):
event.preventDefault()
label = text.strip()
if not label:
return
task = {"id": next_id.current, "label": label, "done": False}
next_id.current += 1
set_tasks(lambda previous: [*previous, task])
set_text("")
def toggle(task_id):
set_tasks(lambda previous: [
{**task, "done": not task["done"]}
if task["id"] == task_id else task
for task in previous
])
return html.main(
html.h1("Tasks"),
html.form(
html.label("New task", html_for=field_id),
html.input(id=field_id, value=text,
on_change=lambda event: set_text(str(event.target.value))),
html.button("Add", type="submit"),
on_submit=add,
),
html.ul([
html.li(
html.label(
html.input(type="checkbox", checked=task["done"],
on_change=lambda event, task_id=task["id"]: toggle(task_id)),
html.span(task["label"]),
), key=task["id"],
) for task in tasks
]),
html.p(f"{sum(task['done'] for task in tasks)} completed", role="status"),
)
Why these choices matter¶
- The updater receives the latest list rather than relying on a stale closure.
- The new record is captured before the ref changes again.
- The handler captures each task ID through a default argument, avoiding Python's late-binding loop closure problem.
- Each list item has a record key, preserving identity if items move.
use_idprovides the form label/field association without a global hardcoded ID.
Try adding filtering or deletion next. Keep the source list as state and derive the visible list during render. This example does not persist tasks across a page reload; add a deliberate storage effect or a backend endpoint if required.