Managing State Nicegui
NiceguiAt work I started using Nicegui quite extensively to build various tools based on it. I’ve found that Nicegui is incredibly versatile to quickly create small to medium sized tools that go beyond the standard data presentation that Streamlit or Plotly Dash allow.
Recently, I came across a discussion about effective state management within Nicegui. The problem is that the library does not provide any out-of-the-box solution, so I’ve developed my own over time. I invite you to read the discussion thread, but the essence of my approach is documented below.
Starting out with Nicegui, you can quickly end up with messy code that mixes frontend and backend logic. You end up losing track of the data and application state, which makes maintaining or expanding on the tool unnecessarily difficult.
After many refactoring sprints, I’ve landed with this general structure for tools:
tool/
├── service/
│ └── ... # Service layer functions
├── ui/
│ └── ... # High level component classes
├── page.py # ToolPage class & UI entrypoint
├── state.py # ToolState class (Signals, Computed, Effects)
└── models.py # Domain models & DTOs
The Page
The page class is the entrypoint for the tool. It initiates the state, high-level ui components, and communicates with the service layer.
It looks something like this:
class ToolPage:
"""This is a tool."""
def __init__(self):
self.state = ToolState()
self._setup_ui()
self._rerun_calculation_effect = Effect(self._calculate) # <- We'll talk about this later
# Register cleanup on disconnect
ui.context.client.on_disconnect(self.cleanup)
def _setup_ui(self):
# Here we initialise our high-level component classes
# They each receive the state and relevant callbacks
self.inputs = Inputs(self.state, self._calculate)
self.chart = Chart(self.state)
self.table = Table(self.state)
async def _calculate(self):
is_running = self.state.is_running()
if is_running:
return
with self.notification_container:
notification = ui.notification(
"Running ...", type="info", timeout=None, spinner=True
)
try:
result = await run.io_bound(...)
self.state.result.set(result)
notification.message = "Done!"
notification.type = "positive"
finally:
notification.timeout = 1
notification.spinner = False
def cleanup(self):
"""Cleanup all resources when client disconnects."""
# Dispose reactive effects
self._rerun_calculation_effect.dispose()
# Cleanup UI components
self.chart.cleanup()
self.table.cleanup()
# Cleanup state
self.state.cleanup()
State
By now you are probably wondering what this state class is. It’s a declarative and reactive state. That means changes propagate automatically and all state-related logic is centralised. The idea is very similar to state management in reflex.dev, which is another great Python web app framework.
State management requires three basic building blocks:
- Signals: Values that change over time that should trigger state changes.
- Computed: Transforms Signals. It itself can also be used as another Signal.
- Effect: Side effects when Signals change.
A new library that provides exactly that with neat dependency management between these components is reaktiv.
The building blocks are well-illustrated here:

What does that mean in practice? Let’s have a look:
class ToolState:
# User defined values
date_filter: Signal[datetime.date]
text_input: Signal[str]
result: Signal[ToolResult | None] # ToolResult serves as data transfer object between service layer and frontend
is_running: Signal[bool]
has_result: Computed[bool]
transformed_result: Computed[dict[str, float]]
def __init__(self):
# This is where we init the vars and set defaults
self.date_filter = Signal(datetime.date.today())
self.text_input = Signal("")
self.result = Signal(None)
self.is_running = Signal(False)
self.has_result = Computed(lambda: self.result() is not None) # <- has_result is automatically updated when result changes!
self._log_effect = Effect(lambda: print(f"{result=}")) # <- Automatically runs when result changes
self.transformed_result = Computed(self._a_function)
# ...
def cleanup(self):
self._log_effect.dispose()
Components
Now that the state is defined, we can share an instance of the state with any number of components, which can present said state.
Each component should accept only a state and callbacks. To link user defined values to Nicegui control components, we can use Nicegui’s builtin binding. On the other side to display the state, we can make use of reaktiv Effects to trigger updates.