GitHub Viewer
# Async and data fetching
PythonNative runs component work on a standard asyncio loop on a dedicated
application thread. Native UI threads own widgets, scrolling, and animations.
Async components can suspend into a `Suspense` fallback, and async effects and
event handlers are cancelled when their component unmounts.
## The application runtime
Use ordinary asyncio networking, `TaskGroup`, timeouts, and synchronization
primitives. Async handlers can be passed directly: `Button("Save", on_press=save)`.
Use `asyncio.to_thread` for blocking I/O. CPU work should yield cooperatively or
run outside the application interpreter when it needs parallel execution.
`run_async()` inherits the current component's task scope. Use
`runtime.run_application_task()` for work that should survive its initiating
component, such as an application-owned service. A native module promise can
release underlying work when its Python waiter is cancelled.
The entry points you'll use directly:
| Helper | When to use |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| [`pn.run_async(coro)`][pythonnative.run_async] | Schedule a coroutine from sync code (a tap handler, a hook setup function). |
| [`pn.run_blocking(coro)`][pythonnative.runtime.run_blocking] | Drive a coroutine to completion from a plain script or a test. |
| [`pn.runtime.get_loop()`][pythonnative.runtime.get_loop] | Access the loop directly for interop with other asyncio libraries. |
| [`pn.runtime.resolve_future`][pythonnative.runtime.resolve_future] | Deliver a value to an `asyncio.Future` from any thread. |
You'll most often call `pn.run_async` from a sync button handler:
```python
import pythonnative as pn
@pn.component
def Toolbar() -> pn.Node:
async def export():
report = await build_report()
await save_to_disk(report)
return pn.Button("Export", on_press=lambda: pn.run_async(export()))
```
## Async components + Suspense
A component body may be an `async def`. The body awaits whatever it
needs and returns its element tree; while an await is pending, the
render **suspends** and the nearest
[`Suspense`][pythonnative.Suspense] boundary shows its fallback:
```python
@pn.component
async def Profile(user_id: str) -> pn.Node:
user = await api.fetch_user(user_id)
return pn.Column(
pn.Text(user["name"]),
pn.Text(user["bio"]),
)
@pn.component
def ProfileScreen() -> pn.Node:
return pn.Suspense(
Profile(user_id="42"),
fallback=pn.ActivityIndicator(),
)
```
No `loading` flag, no conditional render, no effect that copies data
into state. Three timing rules, matching React:
- **Initial mount**: the fallback shows until the content is ready.
- **Updates**: a component that's already on screen and suspends again
(its inputs changed) keeps its previous content visible and
re-renders when the new data arrives; there's no fallback flash.
- **A new descendant suspends**: when a boundary with committed content
gains a child that suspends, the boundary keeps its content mounted
and hidden (`display: "none"`) while the fallback shows, then lifts
the override when the data arrives. Hook state, native views, focus,
and scroll position of the siblings survive, so a `TextInput` next to
the loading widget keeps its text and its keyboard.
Boundaries nest: each `Suspense` covers exactly the subtree it wraps,
so one slow widget doesn't blank the whole screen. A boundary without
a `fallback` is transparent and lets the suspension propagate to the
next boundary up.
The reconciler steps an `async def` body once synchronously and only
schedules a task (and suspends) if the first `await` is actually
pending. Awaits on already-resolved values therefore complete inline
during the render pass: a re-render of an async component whose data
is cached costs no event-loop round trip and shows no fallback.
## Fetch-during-render: `use_resource`
`use_resource(fetcher, deps)` starts an async fetch **during render**
(not after commit, so there's no mount-then-fetch waterfall) and
caches the resulting [`Resource`][pythonnative.Resource] until `deps`
change:
```python
@pn.component
def UserCard(user_id: str) -> pn.Node:
resource = pn.use_resource(lambda: api.get_user(user_id), [user_id])
user = resource.read() # suspends while pending
return pn.Text(user["name"])
```
Consume a resource two ways, with identical semantics:
- `resource.read()` in a regular component: returns the value when
ready, re-raises the fetcher's error (so an enclosing
[`ErrorBoundary`][pythonnative.ErrorBoundary] catches failures),
and suspends the render while pending.
- `await resource` inside an `async def` component.
Because results are cached per component instance, re-renders resolve
instantly; only genuinely new data (a `deps` change) refetches, and
the stale in-flight fetch is cancelled first.
For module-level preloading (start fetching before a screen mounts),
use [`pn.start_resource`][pythonnative.start_resource], the non-hook
constructor.
## Code splitting: `lazy`
[`pn.lazy(loader)`][pythonnative.lazy] defers loading a component
until its first render. The loader runs once; renders suspend until
it resolves:
```python
Chart = pn.lazy(lambda: __import__("app.chart", fromlist=["Chart"]).Chart)
@pn.component
def Dashboard() -> pn.Node:
return pn.Suspense(
Chart(points=[1, 2, 3]),
fallback=pn.ActivityIndicator(),
)
```
Synchronous loaders (a deferred `import`) resolve immediately and
never suspend; `async def` loaders can pull code or data from
anywhere.
## Async side effects: `use_effect` with `async def`
[`use_effect`][pythonnative.use_effect] accepts coroutine callbacks
directly. The coroutine runs as a task on the framework loop after
the commit, and the task is **cancelled automatically** whenever
`deps` change or the component unmounts:
```python
@pn.component
def WelcomeBanner() -> pn.Node:
visible, set_visible = pn.use_state(True)
async def auto_dismiss():
await asyncio.sleep(3.0)
set_visible(False)
pn.use_effect(auto_dismiss, [])
return pn.Text("Welcome!") if visible else pn.Spacer()
```
If the user navigates away within 3 seconds, the sleep is cancelled
and `set_visible` never fires on the unmounted component. An async
effect may also return a callable, which runs as its cleanup:
```python
async def subscribe():
connection = await open_socket()
return connection.close # cleanup
pn.use_effect(subscribe, [])
```
## Keeping the UI responsive: `use_transition` and `use_deferred_value`
State updates are urgent by default: every setter call in one
handler is batched into a single render pass that runs as soon as the
handler returns, so taps and typing feel immediate. When an update
drives *expensive* work (filtering a large list, re-rendering a
chart), mark it as a **transition** so urgent updates queued in the
meantime render first:
```python
@pn.component
def Search() -> pn.Node:
query, set_query = pn.use_state("")
submitted, set_submitted = pn.use_state("")
is_pending, start_transition = pn.use_transition()
def on_change(text):
set_query(text) # urgent: the input updates immediately
start_transition(lambda: set_submitted(text)) # deferred
return pn.Column(
pn.TextInput(value=query, on_change=on_change),
pn.ActivityIndicator() if is_pending else Results(query=submitted),
)
```
`is_pending` is `True` from the moment `start_transition` is called
until the deferred render commits, which is exactly when to show a
lightweight busy hint.
[`use_deferred_value`][pythonnative.use_deferred_value] is the
value-shaped variant: it returns a copy of its argument that lags
behind during bursts and catches up in a transition-priority render
when things go quiet. Pass the deferred copy to the expensive subtree
and the urgent part of the UI stays snappy.
## Loading data with state flags: `use_query`
When you'd rather manage loading state explicitly (or need `refetch`),
`use_query(fetcher, deps)` subscribes to an async fetcher and
re-renders when its result changes. The return value is a frozen
[`QueryResult`][pythonnative.hooks.QueryResult] with `data`,
`loading`, `error`, and a stable `refetch` callable:
```python
@pn.component
def UserCard(user_id: int) -> pn.Node:
q = pn.use_query(lambda: api.get_user(user_id), [user_id])
if q.loading and q.data is None:
return pn.Text("Loading…")
if q.error:
return pn.Text(f"Error: {q.error}")
return pn.Column(
pn.Text(q.data["name"]),
pn.Button("Refresh", on_press=q.refetch),
)
```
Rule of thumb: reach for `use_resource` + `Suspense` when a loading
state is all you need (the boundary owns it), and `use_query` when
the component wants to keep stale data on screen with a refresh
affordance.
Pass an explicit `key=("user", user_id)` to share a request and its cached result
between components. Include every input that identifies that result in the key.
Without an explicit key, the query belongs to its hook instance and refetches
when its dependency list changes; unrelated closures never share results just
because they were defined on the same source line. An optional `client` argument
accepts a `pythonnative.query.QueryClient` with configurable capacity and stale
time. The last subscriber leaving cancels pending work.
## Side-effecting actions: `use_mutation`
Use `use_mutation` for "do something then maybe refresh" patterns
(create, update, delete). It returns a `(state, mutate)` tuple where
`state` is a [`MutationState`][pythonnative.hooks.MutationState] (with
`loading`, `data`, `error`) and `mutate(*args, **kwargs)` triggers the
mutator:
```python
@pn.component
def NewPostForm() -> pn.Node:
title, set_title = pn.use_state("")
state, save = pn.use_mutation(api.create_post)
async def submit():
await save(title) # await the result
set_title("")
pn.Alert.show("Posted!")
return pn.Column(
pn.TextInput(value=title, on_change=set_title),
pn.Button(
"Save" if not state.loading else "Saving…",
on_press=lambda: pn.run_async(submit()),
),
pn.Text(str(state.error)) if state.error else pn.Spacer(),
)
```
The handle returned by `mutate(...)` is a
[`MutationCall`][pythonnative.hooks.MutationCall], awaitable,
cancellable, and safe to ignore if you only care about the state
transitions.
## HTTP requests: `pn.fetch`
A small, dependency-free coroutine wrapper around `urllib`:
```python
resp = await pn.fetch(
"https://api.example.com/posts",
method="POST",
body={"title": "Hello"},
headers={"Authorization": f"Bearer {token}"},
)
resp.raise_for_status()
data = resp.json()
```
`resp.text()`, `resp.json()`, and `resp.content` cover the common
cases. For multipart uploads, HTTP/2, or streaming, integrate
`httpx` / `aiohttp` directly; `pn.fetch` deliberately stays small.
## Key/value persistence: `AsyncStorage`
[`pn.AsyncStorage`][pythonnative.storage.AsyncStorage] is the platform
key/value store (`NSUserDefaults` on iOS, `SharedPreferences` on
Android, a local JSON file in desktop tests). All operations are
coroutines:
```python
await pn.AsyncStorage.set("token", token)
token = await pn.AsyncStorage.get("token")
await pn.AsyncStorage.set_json("user", user.to_dict())
restored = await pn.AsyncStorage.get_json("user")
```
`set_json` / `get_json` add a JSON encode/decode step so you can
round-trip lists, dicts, and primitives.
## Persisted component state: `use_persisted_state`
If you just want `use_state` that survives app restarts, reach for
`use_persisted_state(key, initial)`. It looks like `use_state` but
loads the previous value on mount and writes every update back to
`AsyncStorage`:
```python
@pn.component
def ThemeToggle() -> pn.Node:
theme, set_theme = pn.use_persisted_state("settings.theme", "light")
return pn.Button(
f"Theme: {theme}",
on_press=lambda: set_theme("dark" if theme == "light" else "light"),
)
```
The initial render returns the `initial` fallback; the async load
triggers a re-render with the stored value as soon as it lands.
## Awaitable alerts
[`pn.Alert.confirm`][pythonnative.alerts.Alert.confirm] and
[`pn.Alert.choose`][pythonnative.alerts.Alert.choose] are coroutines
that resolve to the user's choice:
```python
if await pn.Alert.confirm("Delete this item?"):
await delete_item()
photo_source = await pn.Alert.choose(
"Photo source",
options=["Camera", "Gallery"],
cancel_label="Cancel",
)
if photo_source == "Camera":
path = await pn.Camera.take_photo()
```
For a fire-and-forget single-button notice, use the sync
[`pn.Alert.show`][pythonnative.alerts.Alert.show]:
```python
pn.Alert.show("Saved!")
```
## Testing async code
Synchronous headless tests can drive a local standard event loop explicitly:
```python
from pythonnative.runtime import drain, run_blocking
def test_load():
rec.mount(pn.Suspense(Profile(user_id="42"), fallback=pn.Text("…")))
drain() # settle pending tasks
rec.flush_dirty() # commit renders queued by async completions
def test_storage():
assert run_blocking(pn.AsyncStorage.get("token")) is None
```
`async` tests can just `await` framework APIs directly;
[`get_loop`][pythonnative.runtime.get_loop] adopts the already-running
loop.
## How everything fits together
Putting the pieces side by side, here's the canonical "screen with
data + form + persistence + animations" shape:
```python
import pythonnative as pn
@pn.component
async def PostList(user_id: int, version: int) -> pn.Node:
posts = await pn.use_resource(lambda: api.list_posts(user_id), [user_id, version])
return pn.FlatList(
data=posts,
render_item=lambda p, _: pn.Text(p["body"]),
)
@pn.component
def PostsScreen(user_id: int) -> pn.Node:
draft, set_draft = pn.use_persisted_state(f"draft.{user_id}", "")
state, create = pn.use_mutation(api.create_post)
version, set_version = pn.use_state(0)
opacity = pn.use_animated_value(0.0)
pn.use_effect(
lambda: pn.Animated.timing(opacity, to=1.0, duration=300),
[],
)
async def submit():
if not draft.strip():
return
await create({"author_id": user_id, "body": draft})
set_draft("")
set_version(version + 1) # refetch the list resource
return pn.Animated.View(
pn.TextInput(value=draft, on_change=set_draft),
pn.Button(
"Post" if not state.loading else "Posting…",
on_press=lambda: pn.run_async(submit()),
),
pn.Suspense(
PostList(user_id=user_id, version=version),
fallback=pn.ActivityIndicator(),
),
style={"opacity": opacity, "padding": 16, "gap": 8},
)
```
Each piece (fetch, animation, persistence, mutation) is its own
hook with its own lifecycle, and `asyncio` is the glue, running on the
application thread.