NiceGUI Interaction Mechanics¶
Use this reference for user-driven and live application behavior: page and client lifetime, value validation, form submission, uploads, explicit refreshes, timers, application events, background execution, and server-pushed updates. Component-specific event names, scoped event payloads, and Quasar model contracts are covered in component mechanics. Binding graph behavior and typed projections are covered in binding dataclasses.
The NiceGUI implementation details below are verified against NiceGUI 3.16.0. FastAPI's native EventSourceResponse and ServerSentEvent APIs require FastAPI 0.135.0 or later. Check the target application's pinned versions before depending on those surfaces.
Interaction Boundary Map¶
| Boundary | Owns | Does not own |
|---|---|---|
| NiceGUI element | browser-facing value, enabled state, validation display, and registered UI callbacks | domain authorization, durable persistence, or cross-worker coordination |
Page Client |
one page visit's elements, UI context, socket connection, outbox, and client-scoped storage | durable user identity or shared application state |
| Page state | current filters, drafts, selections, busy flags, and serializable projections | database transactions or durable job state |
| Service or repository | domain validation, authorization, transactions, idempotency, and persistence | direct creation or mutation of NiceGUI elements |
| NiceGUI task utility | scheduling work in the event loop, a thread, or a process | durable delivery after process failure |
| FastAPI route | HTTP, SSE, or custom WebSocket protocol and authentication boundary | automatic synchronization with NiceGUI elements |
NiceGUI already uses a Socket.IO connection to carry element events and server updates for each client. Ordinary page interactions should use component callbacks, bindings, Event, and element updates rather than introducing a second transport.
Page And Client Lifetime¶
A @ui.page builder creates a private Client and element tree for each page visit. During initial page construction, Python can create elements before the browser socket exists. Code that requires JavaScript, tab storage, or post-response work must first await ui.context.client.connected().
The tagged page implementation distinguishes two phases:
- Before connection, the page builder must produce the initial response within
response_timeout, which defaults to three seconds. - Once
connected()is awaited, NiceGUI can send the initial HTML immediately and let the remaining async builder continue with a live client.
Long service calls should not delay initial page construction. Render a stable loading state, await the connection where necessary, then perform the asynchronous work and update or refresh the bounded result region.
Disconnect, Reconnect, And Delete¶
The tagged Client implementation treats a transient socket disconnect differently from client deletion:
on_disconnectruns whenever the socket disconnects, including interruptions followed by reconnection.- NiceGUI keeps the client alive for the page's
reconnect_timeout. - A successful handshake within that window cancels pending deletion.
on_deleteruns only when the client is actually removed after the reconnect window or explicit cleanup.- Deletion removes the client's elements and bindings and stops its outbox.
Use on_disconnect for connection telemetry and reversible transport state. Use on_delete to release resources owned by the page visit. Do not close a page-owned resource on every disconnect if it must survive a short reconnect.
NiceGUI's tagged Outbox retains recent messages according to message_history_length and the reconnect window. A reconnecting client supplies its next expected message ID; NiceGUI replays retained messages or reloads the page when the required history is unavailable. Message replay is transport recovery, not a durable event log or a substitute for idempotent service operations.
State Scope¶
app.storage offers scopes with different navigation and process lifetimes:
| Scope | Shared with | Survives page navigation or reload | Persistence notes |
|---|---|---|---|
client |
current page visit only | no | server memory; appropriate for short-lived page resources |
tab |
current browser tab | yes | server memory by default; requires an established connection |
user |
tabs carrying the same signed session ID | yes | server-side persistent dictionary; requires storage_secret |
browser |
tabs sharing the session cookie | yes | cookie payload; writable only before the response is built; prefer user for most data |
general |
all users in the process or configured backend | yes | shared persistent dictionary; not a per-user boundary |
The tagged storage implementation stores general and user data in local JSON files by default or Redis when configured. Tab storage is process memory unless Redis is configured. Multiple workers therefore require an explicitly shared backend for state that must cross processes.
Values, Validation, And Submission¶
NiceGUI value elements mirror browser changes into Python and then invoke on_change or on_value_change handlers. For text input, ui.input sends on_change on each value change unless a Quasar debounce prop delays the model update. Use an enter, blur, or explicit submit event when every keystroke should not trigger application work.
The tagged ValidationElement implements NiceGUI's Python validation:
- a callable returns an error string or
None - a dictionary maps error strings to predicates and stops at the first failed predicate
- automatic validation runs after each handled value change unless
without_auto_validation()is set validate()updates the element'serroranderror-messageprops- asynchronous validation runs as a background task;
validate(return_result=True)is not supported for an async validator
NiceGUI validation is suitable for field feedback, but a submit operation still needs service-level validation and authorization. Browser values, client-side Quasar rules, file metadata, and hidden or disabled controls are not trust boundaries.
NiceGUI does not require a transport-level HTML form for ordinary page submission: current element values already exist in Python. A submit handler can validate relevant fields, construct an immutable command or DTO, call the service boundary, and update the page from the accepted result. Clear draft state only after persistence succeeds.
async def submit() -> None:
if not all(field.validate() for field in (name, email)):
return
submit_button.disable()
try:
user = await user_service.create(name=name.value, email=email.value)
ui.notify(f"Created {user.display_name}", type="positive")
name.set_value("")
email.set_value("")
except DuplicateEmailError:
email.error = "This email is already registered"
finally:
submit_button.enable()
For asynchronous field validators, await the validator at the service boundary or maintain an explicit validation state; do not use the synchronous return value of validate() as proof that asynchronous validation completed.
Upload Mechanics¶
ui.upload wraps Quasar's QUploader. The tagged Upload wrapper registers a POST route scoped to the current client and element. Its event order is:
on_rejectedduring browser-side file selection for Quasar restrictions.on_begin_uploadwhen the client starts a request.on_uploadonce for each server-received file.on_multi_uploadafter all files in that request have been converted.
max_file_size, max_total_size, max_files, and an accept prop improve client feedback, but NiceGUI's security guidance identifies those restrictions as browser-side checks. Revalidate size, media type, content signature, filename policy, authorization, and storage quota on the server before persisting or parsing data.
In NiceGUI 3.16, event.file is a FileUpload:
| Surface | Behavior |
|---|---|
name |
basename sanitized by NiceGUI; still untrusted display metadata |
content_type |
request-provided media type; not content verification |
size() |
synchronous byte count |
read(), text(), json() |
asynchronous full-content reads |
iterate(chunk_size=...) |
asynchronous chunks for bounded-memory processing |
save(path) |
asynchronous save to an application-selected path |
NiceGUI reads the incoming Starlette upload and keeps it in memory up to MultiPartParser.spool_max_size; larger files spill to a temporary file. This spool threshold controls memory versus disk, not the allowed upload size. Raising it increases per-upload memory pressure and should not be used as a validation mechanism.
from nicegui import events, ui
async def handle_upload(event: events.UploadEventArguments) -> None:
file = event.file
if file.size() > 10 * 1024 * 1024:
ui.notify("File exceeds 10 MB", type="negative")
return
if file.content_type != "application/pdf":
ui.notify("Only PDF files are accepted", type="negative")
return
try:
await file_service.store(chunks=file.iterate(), original_name=file.name)
except StorageQuotaError:
ui.notify("Storage quota exceeded", type="negative")
else:
ui.notify(f"Uploaded {file.name}", type="positive")
uploader = ui.upload(
on_upload=handle_upload,
on_rejected=lambda: ui.notify("File rejected", type="negative"),
max_file_size=10 * 1024 * 1024,
auto_upload=True,
).props("accept=application/pdf")
Generate the durable storage name independently from file.name, keep user-uploaded active content off the application origin, and apply content-specific scanning before downstream parsers consume the file. Call uploader.reset() when the product should clear QUploader's client-side queue after a completed or abandoned operation.
Refreshable Component Regions¶
The reusable component factory pattern combines stable bindable fields with bounded structural refreshes. Use bindings and setters while an existing element can represent the change; use a refreshable region when the number, type, order, or nesting of child elements must be rebuilt.
Use the narrowest update mechanism that represents the change:
| Change | Appropriate surface |
|---|---|
| one wrapper property | setter, binding, or property assignment supported by that wrapper |
| mutated option or row collection | wrapper helper or explicit element.update() |
| a bounded subtree whose structure changed | @ui.refreshable or @ui.refreshable_method |
| navigation to a different page | ui.navigate or ui.sub_pages |
The tagged refreshable implementation records every invocation as a target containing a RefreshableContainer, the function arguments, and the associated object instance when applicable. The initial call both renders the region and registers that target. Calling refresh() before the decorated function or method has rendered does nothing because no target exists yet.
For each matching target, refresh() clears the container, updates its remembered arguments, and invokes the function again inside that same container. It recreates the subtree rather than diffing children. Bindings and event handlers owned by deleted elements follow normal element cleanup; a retained reference to a former child does not become the new child. Expose component state and public actions through the returned component handle instead of leaking refresh-owned element references.
Function And Method Scope¶
Choose the decorator according to state ownership:
| Form | Target identity | Appropriate scope |
|---|---|---|
module-level @ui.refreshable |
every surviving call target of that decorated function | deliberate multicast or shared rendering |
page-local @ui.refreshable |
calls recorded by the function created during that page build | one page client |
page-created ui.refreshable(function) |
calls recorded by that decorated wrapper | one page client or component factory call |
@ui.refreshable_method |
targets whose recorded instance equals the accessed object | reusable component instances with independent state |
A module-level refreshable called by multiple clients has multiple targets, so one refresh can update every surviving target. The official global and local scope examples demonstrate this distinction. For reusable components returned as dataclass handles, prefer a page-created instance with @ui.refreshable_method; NiceGUI's tagged multi-instance tests verify that refreshing one instance selects its own targets.
Calling the same refreshable function more than once creates more than one target. For @ui.refreshable_method, every call made on the same instance belongs to that instance, so instance.region.refresh() refreshes all surviving targets for that method and instance. Use separate methods or separate component instances when independently refreshing two regions is required.
Arguments And Return Behavior¶
Targets remember their initial positional and keyword arguments:
- no refresh arguments reuse all remembered values
- non-empty positional refresh arguments replace the remembered positional tuple
- keyword refresh arguments update the remembered keyword dictionary
- arguments must remain consistently positional or keyword; supplying the same parameter through both paths raises
TypeError - the initial call and each refresh return the decorated function's normal result; the
refresh()wrapper itself exposes NiceGUI's awaitable response behavior
Parameters should describe render input, not hide durable state. On a reusable component, fields on the returned dataclass usually provide a clearer interface than repeatedly replacing a long refresh argument list.
Async Refresh¶
An async refreshable's initial invocation returns its coroutine and should be awaited when page construction depends on its output. For subsequent refreshes:
await region.refresh()waits for all matching async refreshes to finish- calling
region.refresh()without awaiting schedules the async work in the background - awaiting is appropriate when a button must remain disabled until rendering completes
- each refresh clears the old target before the new async render finishes, so provide a stable outer loading surface when an empty interval would be disruptive
Multiple matching targets are refreshed together; awaiting waits for all async results through asyncio.gather. That coordinates completion but does not serialize competing refresh calls. Apply the generation, lock, or coalescing policy described under concurrency and feedback state when two operations can refresh the same target concurrently.
Target And Local-State Lifetime¶
Before every invocation or refresh, NiceGUI prunes targets whose container was deleted. Clearing an ancestor, navigating away, deleting the client, or replacing an outer refreshable region can therefore remove an inner target. A later call to the inner region's refresh() cannot recreate a pruned outer placement; the owning outer render must invoke it again.
ui.state() stores values in a list owned by one refreshable target and identifies each value by call order. Its setter automatically refreshes the associated instance target. Conditional or reordered ui.state() calls can associate stored values with a different logical variable, so keep their call order stable.
For reusable application components, a bindable dataclass is usually the clearer state owner: fields have explicit names, can bind directly to stable elements, and remain available to the page through the returned handle. Reserve ui.state() for small render-local values that do not need a typed component API, cross-component coordination, service persistence, or independent tests.
Timers And Application Events¶
ui.timer is client-scoped. Its tagged element implementation waits for the client connection and cancels the current invocation when the element is deleted. app.timer is application-scoped and has no UI context of its own.
The tagged base Timer awaits each callback before scheduling the remainder of the interval, so one timer does not overlap its own invocations. A callback that takes longer than the interval causes the next iteration to begin without an additional delay. deactivate() pauses future invocations, while cancel(with_current_invocation=True) also cancels the current callback task and cannot be reversed.
Use timers for truly periodic observation, not to compensate for a missing event or explicit refresh. Polling intervals must account for query cost, number of connected clients, and process-local duplication under multiple workers.
Event decouples long-lived Python producers from UI subscribers:
emit()invokes subscribers without waiting for async callbacks to completecall()awaits all subscribers and propagates their failures to the calleremitted(timeout=...)waits for the next emission- subscriptions created in a UI context are automatically removed when that client is deleted unless configured otherwise
The automatic unsubscribe behavior in the tagged Event implementation makes an application event suitable for connecting longer-lived models to page-local UI without retaining deleted clients. It remains process-local; use a broker or shared service for cross-worker fan-out.
Execution Contexts¶
Choose an execution surface by workload and lifetime:
| Surface | Execution | Suitable for | Important constraint |
|---|---|---|---|
| async UI handler | event loop | non-blocking clients and short orchestration | blocking calls freeze all clients on that loop |
run.io_bound() |
shared thread pool | blocking file, HTTP, or SDK calls | cancellation does not necessarily stop the underlying thread operation |
run.cpu_bound() |
process pool | CPU-heavy pure computation | callable, arguments, result, and failures cross a pickle boundary |
background_tasks.create() |
event-loop task | detached async work owned by this process | canceled during shutdown unless tagged with await_on_shutdown |
FastAPI BackgroundTasks |
after an HTTP response | small route-triggered work | still belongs to the web process; not a durable queue |
| external worker or job queue | separate process or service | durable, retryable, resource-heavy jobs | requires explicit status, cancellation, and result contracts |
The tagged run implementation uses a thread pool for io_bound and a process pool for cpu_bound. For CPU work, prefer a module-level function with simple serializable arguments and return data rather than UI objects or closures. NiceGUI 3.16 inherits the platform multiprocessing start method unless run.process_pool_start_method is set before startup; spawn avoids unsafe fork behavior in a threaded process but does not inherit module state.
The tagged background_tasks implementation keeps strong references to running tasks, forwards unhandled exceptions to global exception handlers, and cancels ordinary tasks during shutdown. create_lazy() coalesces repeated work by name into the current run plus only the latest waiting coroutine; it is useful for refresh-style invalidation, not for work where every event must be processed.
Decorate a coroutine with @background_tasks.await_on_shutdown only when process shutdown must wait for that bounded task to finish, such as flushing a small already-accepted result. The decorator prevents NiceGUI's normal shutdown cancellation; it does not make the work durable after a crash, container kill, or host failure. Keep unbounded work and retryable jobs in an external worker rather than delaying application termination indefinitely.
Live Update Transports¶
| Requirement | Default surface |
|---|---|
| update the initiating NiceGUI page | mutate elements or bound page state in its client context |
| notify all local clients of a page | iterate app.clients(path) and enter each with client: context |
| connect a long-lived Python producer to page subscribers | NiceGUI Event with page-local subscriptions |
| one-way HTTP event stream for an external/browser consumer | FastAPI SSE endpoint |
| custom bidirectional protocol independent of NiceGUI elements | FastAPI WebSocket endpoint |
| cross-worker or cross-instance broadcast | external broker plus a subscriber in each process |
FastAPI's SSE support uses a yielding route with response_class=EventSourceResponse. ServerSentEvent adds event, id, retry, and comment fields; event IDs support application-defined resume behavior through Last-Event-ID. FastAPI supplies keep-alive comments and headers that discourage proxy buffering and caching. The stream producer still owns authorization, disconnect-aware resource cleanup, replay semantics, and bounded buffering.
FastAPI WebSockets support text, bytes, and JSON in both directions. Catch WebSocketDisconnect, remove the connection from any local registry, and remember that an in-memory connection manager reaches only clients attached to the same process.
Do not use SSE or a custom WebSocket merely to update NiceGUI elements. Those transports do not automatically establish the target NiceGUI client context or synchronize its element tree.
Concurrency And Feedback State¶
Disabling the initiating control communicates that work is active, but it is not a server-side concurrency guarantee. Also guard the handler or service with one of these policies:
- reject a second request while the operation is in flight
- coalesce duplicate refresh requests and keep only the latest invalidation
- serialize operations with a lock scoped to the affected entity or user
- make the service operation idempotent and return the existing result
For search, filtering, and other replaceable reads, an older request can complete after a newer request. Associate each request with a monotonically increasing generation or cancel the previous task, and only publish a result that still matches the current generation. Cancellation must still restore enabled/loading state in finally.
Every user-triggered asynchronous operation should expose a bounded state model such as idle, running, succeeded, failed, or canceled. Keep the error message near the action, preserve user input after expected failure, and do not convert unexpected programming errors into a generic success-like state.
Source Index¶
NiceGUI public documentation
NiceGUI 3.16.0 implementation
FastAPI transports and tasks