Managing state #4758
Replies: 4 comments 24 replies
-
|
Hello @FabianGoessling. Let me help you as much as possible in getting over the NiceGUI learning curve. First of all, unlike Reflex, our front-end and back-end is much more "close together". Our page functions BOTH does back-end calculations, as well as populating the front-end with This is an important realization to make. This is because, if I am not mistaken, in Reflex, you're not supposed to return UI elements in yout However, in NiceGUI, you can imagine each page as one Python function, which just so happens to have some So, in short, I'd say Reflex does full-stack by having you be both a front-end and a back-end engineer, while NiceGUI let any browser be the frontend of your Python code while you focus on the backend only. From what I see, 3 choices:
@ui.page('/hello')
def hello_page():
hello_string = generate_hello_string() # see wht I did here
ui.label(hello_string)This way, on every page load, the string is loaded in
from nicegui import ui, app
app.storage.general['hello_string'] = generate_hello_string() # see wht I did here
@ui.page('/hello')
def hello_page():
ui.label().bind_text_from(app.storage.general, 'hello_string') # see wht I did here
from nicegui import ui, app
@ui.page('/hello')
def hello_page():
ui.input(load_from_SQLite(), on_change=lambda e: save_to_SQLite(e.value))However, you need to be more specific for me to provide any more specific recommendations. If you don't mind, you can open-source your application, then me and others will raise issues and PRs if I have spare time. You may even get featured on the NiceGUI examples! Alternatively, if you'd like not to do that, you would need to boil-down your code to only the problematic parts and provide a MRE. https://github.com/zauberzeug/nicegui/wiki/FAQs#why-is-it-important-to-provide-a-minimal-reproducible-example |
Beta Was this translation helpful? Give feedback.
-
|
Hey @FabianGoessling, I stumbled upon your discussion about state management in NiceGUI, and I thought my library reaktiv might help with this issue. I'm not a NiceGUI expert by any means, but I have extensive experience with Angular and similar reactive frameworks. When I faced similar state management challenges in my Python applications, I ended up creating Here's how I've been using it with NiceGUI in my internal tools: # install reaktiv with `pip install reaktiv`
from reaktiv import Signal, Computed, Effect
from nicegui import ui
# State module - completely independent from UI
class TodoState:
def __init__(self):
self.todos = Signal([])
self.filter = Signal("all") # all, active, completed
self.filtered_todos = Computed(lambda: [
todo for todo in self.todos()
if self.filter() == "all"
or (self.filter() == "active" and not todo["completed"])
or (self.filter() == "completed" and todo["completed"])
])
self.active_count = Computed(lambda:
sum(1 for todo in self.todos() if not todo["completed"])
)
self.completed_count = Computed(lambda:
sum(1 for todo in self.todos() if todo["completed"])
)
def add_todo(self, text):
self.todos.update(lambda todos: todos + [{"text": text, "completed": False}])
def toggle_todo(self, index):
self.todos.update(lambda todos: [
{**todo, "completed": not todo["completed"]} if i == index else todo
for i, todo in enumerate(todos)
])
def clear_completed(self):
self.todos.update(lambda todos: [todo for todo in todos if not todo["completed"]])
# Create a state instance
state = TodoState()
# UI layer can now use the state
with ui.card():
ui.label("Todo App").classes("text-xl")
# Input for new todos
with ui.row():
new_todo = ui.input("New task")
ui.button("Add", on_click=lambda: [state.add_todo(new_todo.value), new_todo.set_value("")])
# Todo list - connected to state via Effect
todo_container = ui.column()
def render_todos():
todo_container.clear()
for i, todo in enumerate(state.filtered_todos()):
with todo_container:
with ui.row():
ui.checkbox(value=todo["completed"], on_change=lambda e, idx=i: state.toggle_todo(idx))
ui.label(todo["text"]).classes("line-through" if todo["completed"] else "")
# Effect connects state to UI
render_effect = Effect(render_todos)
# Filter controls
with ui.row():
ui.button("All", on_click=lambda: state.filter.set("all"))
ui.button("Active", on_click=lambda: state.filter.set("active"))
ui.button("Completed", on_click=lambda: state.filter.set("completed"))
ui.button("Clear completed", on_click=lambda: state.clear_completed())
# Status display - automatically updates
status_label = ui.label()
status_effect = Effect(lambda: status_label.set_text(
f"{state.active_count()} active, {state.completed_count()} completed"
))
ui.run()What I love about this approach is that I've completely separated my state logic from the UI. I hope this will be interesting for some people. |
Beta Was this translation helpful? Give feedback.
-
|
Hi there, i just stumbled across https://github.com/CrystalWindSnake/ex4nicegui which implements the reactive pattern for nicegui including v-for and so on ( better look at the english Readme). With this added nicegui would behave like reflect and give a more vue native experience. With the latest feats regarding reactive props, is there any roadmap to get rid of the boilerplate ui.refreshable and manual updating all together with eg a v-for implementation? Looking forward to your ideas 🙂 |
Beta Was this translation helpful? Give feedback.
-
|
I'd love to see more discussion around structuring nicegui apps. Below are my two cents. I've been using @buiapp 's reaktiv extensively to build various apps. I've not looked at reflex much, but the way I structure my app is (unintentionally) very close to what it recommends. The benefits are:
Below is an example. I have a page class that initiates the state, the ui components, and communicates with the service layer. I won't include service layer code here as it's essentially just calculation, io, etc. class FixingsPage:
"""Page to forecast ee spreads."""
def __init__(self):
self.state = FixingsState()
self._setup_ui()
self._rerun_forecast_effect = Effect(self._calculate_forecast)
# Register cleanup on disconnect
ui.context.client.on_disconnect(self.cleanup)
def _setup_ui(self):
apply_theme()
self.inputs = Inputs(self.state, self._calculate)
self.chart = Chart(self.state)
self.table = Table(self.state)
self.notification_container = ui.element()
async def _calculate(self):
is_running = self.state.is_running()
if is_running:
return
# ...
with self.notification_container:
notification = ui.notification(
"Running ML model...", 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."""
logger.debug("Cleaning up FixingsPage resources")
# Dispose reactive effects
if hasattr(self, "_rerun_forecast_effect"):
self._rerun_forecast_effect.dispose()
# Cleanup UI components
self.chart.cleanup()
self.table.cleanup()
# Cleanup state
self.state.cleanup()
def page():
"""Entrypoint."""
FixingsPage()The state holds all the page's data and computes general transformations of the backend data for the frontend. class FixingsState:
"""State for fixings page."""
date_filter: Signal[datetime.date]
spread_type: Signal[SpreadType]
run_model: Signal[bool]
run_in_separate_process: Signal[bool]
result: Signal[FixingsResult | None]
is_running: Signal[bool]
latest_market: Computed
market_forwards: Computed
saved_forwards: LinkedSignal[pd.DataFrame | None]
editable_forwards: Computed
has_result: Computed
def __init__(self):
self.date_filter = Signal(datetime.date.today())
self.spread_type = Signal(SpreadType.Estr3s)
self.run_model = Signal(True)
self.run_in_separate_process = Signal(False)
self.request = Signal(None)
self.result = Signal(None)
self.is_running = Signal(False)
self._live_market_signal = get_market_signal(
...
)
self.latest_market = Computed(
self._compute_latest_market
)
self.market_forwards = Computed(self._compute_market_forwards)
self.saved_forwards = LinkedSignal(self._compute_saved_forwards)
self.editable_forwards = Computed(self._compute_editable_forwards)
self._save_effect = Effect(self._save_manual_forwards)
self.has_result = Computed(lambda: self.result() is not None)
# ...
def cleanup(self):
"""Cleanup reactive effects."""
logger.debug("Cleaning up FixingsState")
if hasattr(self, "_save_effect"):
self._save_effect.dispose()Finally, every major ui section gets its own class as seen in the page class. Below is the inputs class for example: class Inputs:
"""Inputs for Fixings page."""
def __init__(
self, state: FixingsState, calculate_callback: Callable[[], Awaitable[None]]
):
self.proxy = ReactiveProxy.from_object(state)
self.calculate_callback = calculate_callback
self._setup_ui()
def _setup_ui(self):
with ui.card().classes("gap-6 rounded-xl shadow-sm border"):
with ui.card_section().classes("pb-0"):
ui.label("EE Forecast").classes("text-lg font-semibold")
with ui.card_section().classes("pt-0"):
with ui.row().classes("justify-end gap-2 mt-0 items-center"):
ui.select(
options={
spread_type: spread_type.to_string()
for spread_type in SpreadType
},
label="Model Type",
value=SpreadType.Estr3s,
).bind_value_to(self.proxy, "spread_type").classes("w-32")
with (
ui.input(
"Start Date",
value=datetime.date.today().strftime("%Y-%m-%d"),
)
.bind_value(
self.proxy,
"date_filter",
forward=lambda val: datetime.datetime.strptime(
val, "%Y-%m-%d"
).date()
if val and val.strip()
else None,
backward=lambda dt: dt.strftime("%Y-%m-%d") if dt else "",
)
.classes("pt-0") as date
):
with ui.menu().props("no-parent-event") as menu:
with (
ui.date()
.props('minimal first-day-of-week="1"')
.bind_value(date)
) as self.date_select:
with ui.row().classes("justify-end"):
ui.button(
"Today", on_click=self._set_date_to_today
).props("flat")
ui.button("Close", on_click=menu.close).props(
"flat"
)
with date.add_slot("append"):
ui.icon("edit_calendar").on("click", menu.open).classes(
"cursor-pointer"
)
ui.switch("Run Model").bind_value(self.proxy, "run_model")
ui.switch("Separate Process").bind_value(
self.proxy, "run_in_separate_process"
)
ui.button("Calculate", on_click=self.calculate_callback).classes(
"ml-4"
)
def _set_date_to_today(self):
self.date_select.value = datetime.date.today().strftime("%Y-%m-%d")The ReactiveProxy class is a translation layer between nicegui's BindableProperties and reaktiv. This makes both systems easily compatible and avoids nicegui's default polling behaviour. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Hey there,
i am using nicegui for almost three years now, but am still struggling with properly managing my application state in a clean and concise way.
Prior to using nicegui I was building the frontend using vue.js and the backend using fastapi/Django. The split between frontend and backend of course made coding way more complicated but I was always sure where my variables "lived" - some in my vue.js data prop and some in the backend.
Using nicegui my code always ends up in a situation where I cannot use the backend without frontend and vice versa. This contradicts the idea that my backend is setup as an API, which should be usable (and testable) standalone.
Recently I read the documentation of reflex.dev and they are very clear on the matter of the application state. They have a state class which lives in the backend side and is automatically refreshed/bound.
How would i manage state best in your opinion? How are you doing it for larger apps? Can I adopt the reflex pattern with nicegui or is there a conceptional difference?
Moreover I am wondering if there is a possibility to create variables which are only on the frontend side (e.g. an "is_visible" or an "is_calculating"). Is it possible or are all variables always updated in the backend?
Looking forward to your approaches,
Thanks
Fabian
Beta Was this translation helpful? Give feedback.
All reactions