# 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](./component-mechanics.md). Binding graph behavior and typed projections are covered in [binding dataclasses](./binding-dataclasses.md). 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`](https://nicegui.io/documentation/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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py) 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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py) 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`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py) 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`](https://nicegui.io/documentation/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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/storage.py) 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`](https://nicegui.io/documentation/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`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/validation_element.py) 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. ```python 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`](https://nicegui.io/documentation/upload) wraps Quasar's `QUploader`. The tagged [`Upload` wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py) 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](https://nicegui.io/documentation/section_security#examples_are_starting_points) 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`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py): | 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. ```python 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](./architecture.md#reusable-component-contract) 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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py) 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](https://nicegui.io/documentation/refreshable#global_scope) 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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/tests/test_refreshable.py) 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](#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`](https://nicegui.io/documentation/timer) is client-scoped. Its tagged [element implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/timer.py) 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`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py) 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`](https://nicegui.io/documentation/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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event.py) 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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/run.py) 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](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/background_tasks.py) 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](https://fastapi.tiangolo.com/tutorial/server-sent-events/) 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](https://fastapi.tiangolo.com/advanced/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 !!! info "NiceGUI public documentation" - [Pages and client connection](https://nicegui.io/documentation/page) - [Action, events, execution, and error handling](https://nicegui.io/documentation/section_action_events) - [Input and validation](https://nicegui.io/documentation/input) - [Upload](https://nicegui.io/documentation/upload) - [Refreshable UI](https://nicegui.io/documentation/refreshable) - [Timer](https://nicegui.io/documentation/timer) - [Application events](https://nicegui.io/documentation/event) - [Storage scopes](https://nicegui.io/documentation/storage) !!! info "NiceGUI `3.16.0` implementation" - [Page builder and response phases](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py) - [Client lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py) - [Outbox and reconnect replay](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py) - [Validation elements](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/validation_element.py) - [Upload wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py) - [Uploaded-file storage and access](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py) - [Refreshable targets and local state](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py) - [Timer scheduling](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py) - [Application event dispatch](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event.py) - [Thread and process execution](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/run.py) - [Background-task lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/background_tasks.py) !!! info "FastAPI transports and tasks" - [Server-sent events](https://fastapi.tiangolo.com/tutorial/server-sent-events/) - [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/) - [Response background tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/)