Skip to content

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_id provides 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.