Skip to content

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:

  1. Before connection, the page builder must produce the initial response within response_timeout, which defaults to three seconds.
  2. 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_disconnect runs 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_delete runs 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's error and error-message props
  • 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:

  1. on_rejected during browser-side file selection for Quasar restrictions.
  2. on_begin_upload when the client starts a request.
  3. on_upload once for each server-received file.
  4. on_multi_upload after 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 complete
  • call() awaits all subscribers and propagates their failures to the caller
  • emitted(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