Async fetch¶
Fetch data with an async create_memo and show a loading state with Loading. There's no separate resource primitive: a memo whose body is async def is the async computation.
from js import fetch
from wybthon import (
Errored,
Loading,
button,
component,
create_memo,
create_signal,
div,
is_pending,
p,
render,
span,
)
@component
def TodoViewer():
todo_id, set_todo_id = create_signal(1)
async def load_todo():
# Reads are tracked before and after ``await``: changing ``todo_id``
# refetches automatically.
response = await fetch(f"https://jsonplaceholder.typicode.com/todos/{todo_id()}")
if not response.ok:
raise RuntimeError(f"HTTP {response.status}")
return (await response.json()).to_py()
todo = create_memo(load_todo)
return div(
button("Previous", on_click=lambda: set_todo_id(lambda i: max(1, i - 1))),
button("Next", on_click=lambda: set_todo_id(lambda i: i + 1)),
Errored(
lambda: Loading(
lambda: p(
"Todo #",
lambda: todo()["id"],
": ",
lambda: todo()["title"],
span(lambda: " (refreshing...)" if is_pending(todo) else ""),
),
fallback=p("Loading..."),
),
fallback=lambda err, reset: div(p("Failed: ", lambda: str(err())), button("Retry", on_click=reset)),
),
)
render(TodoViewer(), "#app")
How it works¶
Reading todo() inside the boundary is what wires it to Loading. Until the coroutine resolves for the first time, the read raises NotReadyError; the hole keeps its previous content and the nearest boundary shows its fallback. The content stays mounted the whole time (parked off-document), so any state created inside it survives.
Once the memo has a value, the fallback never returns. When todo_id changes, the memo recomputes inside a transition: it keeps serving the previous todo, and the UI that read todo_id holds with it, until the new one arrives, so the id and the todo never disagree on screen. is_pending is True during that window, which drives the inline hint.
If load_todo raises, the error routes to the nearest Errored boundary. Its fallback reads the error through the err accessor, and its reset callback re-renders the children, which re-reads the memo.
Refetching¶
Signal reads inside the async body are dependencies, so any signal works as a refetch trigger. A "version" counter is the simplest:
from wybthon import button, create_memo, create_signal
version, set_version = create_signal(0)
async def load_report():
version() # tracked: bumping it refetches
return await fetch_report()
report = create_memo(load_report)
button("Refetch", on_click=lambda: set_version(lambda v: v + 1))
To refetch quietly after a mutation, without showing a pending state, use refresh. It returns an awaitable for the next settled value:
from wybthon import action, refresh
@action
async def save_report(data):
await post_report(data)
await refresh(report)
Reading outside a boundary¶
latest evaluates an expression without ever raising NotReadyError; unresolved computations yield None:
resolve awaits the next settled value, which is handy in actions and scripts:
Waiting on data the children don't read¶
Pass on= to make a boundary wait for specific accessors even if nothing inside reads them, for example to keep a layout from partially rendering:
from wybthon import Loading, p
Loading(lambda: p("Ready"), fallback=p("Loading..."), on=[user, settings])
Next steps¶
- Read Async and Loading for the full model, including
Revealfor coordinating several boundaries. - See
create_memofor async memo semantics, including async generators. - Browse the Error handling example for failure-state UI.