2025, Oct 15 23:00
How to Keep Multi-Page Taipy Dashboards in Sync: Global State, on_init, and Broadcasted Refreshes
Fix Taipy dashboards that don't refresh: keep global state in main, run on_init per session, and use Gui.broadcast_callback for on-demand Postgres updates.
Refreshing live data in a multi-page Taipy app sounds straightforward until the UI refuses to update. A typical setup with a header page, KPI blocks, and separate Analysis and Overview routes often loads once and stays frozen, leaving the “Refresh Now” button doing nothing visible across sessions. The goal is simple: pull fresh rows from Postgres automatically on a schedule and on demand via a button, and have every connected user see the same updated state.
Problem setup and the non-refreshing pattern
The application loads data for KPIs and two content pages from a shared module. The following code illustrates the pattern where the UI doesn’t reliably refresh for all users and pages.
# data/get_data.py
import psycopg2
import pandas as pd
from datetime import datetime, timedelta
PG_HOST = "localhost"
PG_PORT = "5432"
PG_DBNAME = "test"
PG_USER = "pg_db"
PG_PASSWORD = "Admin"
df_cache = None  # supposed to be shared across pages
day_slice = None
def pull_dataset():
    conn = psycopg2.connect(
        host=PG_HOST,
        port=PG_PORT,
        dbname=PG_DBNAME,
        user=PG_USER,
        password=PG_PASSWORD
    )
    query = "SELECT * FROM prod_db"
    df_cache = pd.read_sql(query, conn)
    df_cache['date'] = pd.to_datetime(df_cache['date'])
    df_cache.columns = ['Date', 'Location', 'Tower Name', 'Zone', 'Stage Label', 'Class Stage Label']
    yesterday = (datetime.now() - timedelta(days=1)).date()
    day_slice = df_cache[df_cache['Date'].dt.date == yesterday]
    day_slice = day_slice.sort_values(by='Class Stage Label', ascending=False)
    conn.close()
    return df_cache, day_slice
def refresh_store(state=None):
    global df_cache, day_slice
    df_cache, day_slice = pull_dataset()
    if state:
        state.df_cache = df_cache
        state.day_slice = day_slice
# main.py
from taipy.gui import Gui
import threading
import time
from data.get_data import refresh_store
from pages import root
from Analysis import analysis
from Overview import overview
# background loop
def tick_loop(state):
    while True:
        time.sleep(300)
        refresh_store(state)
# try to start on GUI init
def boot_refresh(state):
    refresh_store(state)
    threading.Thread(target=tick_loop, args=(state,), daemon=True).start()
routes = {
    "/": home_view,
    "Analysis": analysis_view,
    "Overview": overview_view
}
Gui(pages=routes).run(on_init=boot_refresh)
# pages/root.py
from data.get_data import day_slice, refresh_store
import taipy.gui.builder as tgb
with tgb.Page() as home_view:
    tgb.text('## Performance Insights', mode='md')
    tgb.button('Refresh Now', on_action=refresh_store)
    with tgb.layout("1 1 1 1 1 1"):
        with tgb.part():
            tgb.text('### Date', mode='md')
            tgb.text("#### {','.join(day_slice['Date'].dt.strftime('%Y-%m-%d').unique())}", mode='md')
        with tgb.part():
            tgb.text('### Total', mode='md')
            tgb.text("#### {int(day_slice['Class Stage Label'].count())}", mode='md')
        with tgb.part():
            tgb.text('### Normal Stage', mode='md')
            tgb.text("#### {int(day_slice[day_slice['Class Stage Label']=='Normal'].shape[0])}", mode='md')
        with tgb.part():
            tgb.text('### Stage One', mode='md')
            tgb.text("#### {int(day_slice[day_slice['Class Stage Label']=='Stage1'].shape[0])}", mode='md')
        with tgb.part():
            tgb.text('### Stage Two', mode='md')
            tgb.text("#### {int(day_slice[day_slice['Class Stage Label']=='Stage2'].shape[0])}", class_name="lblink", mode='md')
        with tgb.part():
            tgb.text('### Stage Three', mode='md')
            tgb.text("#### {int(day_slice[day_slice['Class Stage Label']=='Stage3'].shape[0])}", class_name="blink", mode='md')
    tgb.navbar()
    with tgb.expandable(title='Data', expanded=False):
        tgb.table("{day_slice}")
What actually goes wrong
The data objects are loaded and even reassigned, but they aren’t positioned where Taipy expects shared application state to live. Without being in the main module where Gui is created, they aren’t treated as global state shared by all pages. The refresh loop also updates a single session context rather than broadcasting changes. Finally, trying to pass an on_init callback into Gui.run won’t be picked up, because that parameter isn’t supported; Taipy looks for a function named on_init in the main module or accepts assignment via gui.on_init.
The fix: make state global to the app and broadcast refreshes
The corrective approach is to ensure the shared variables are imported into the main module so Taipy treats them as global state, initialize them when a session connects via on_init, and push scheduled updates to every connected user with broadcast_callback. The button stays wired to the same data refresh function.
# main.py
from taipy.gui import Gui
import threading
import time
from data.get_data import refresh_store, df_cache, day_slice
from pages import root
from Analysis import analysis
from Overview import overview
# initialize data when a new session connects
def on_init(state):
    refresh_store(state)
# periodically broadcast refresh to all sessions
def auto_refresher(gui: Gui):
    while True:
        time.sleep(300)
        gui.broadcast_callback(refresh_store)
routes = {
    "/": home_view,
    "Analysis": analysis_view,
    "Overview": overview_view
}
gui = Gui(pages=routes)
thread = threading.Thread(target=auto_refresher, args=(gui,))
thread.start()
gui.run()
This layout does three crucial things. First, df_cache and day_slice live in the main module’s scope, so all pages share the same objects. Second, on_init runs for each new session and populates state immediately. Third, the background thread doesn’t touch a single session; it uses broadcast_callback to execute the refresh function for everyone at once on a five-minute interval.
Why this matters for Taipy dashboards
Dashboards that aggregate data across multiple pages must stay consistent across user sessions. Centralizing shared state in the main module and broadcasting updates guarantees that a manual click on “Refresh Now” and a scheduled refresh converge to the same, app-wide result. It prevents divergent states per user and avoids silent no-op refreshes that don’t trigger UI updates.
Wrap-up and practical advice
Keep the shared data structures in the main module alongside Gui so they become Taipy’s global state. Initialize each session by providing an on_init function and let Taipy discover it automatically. Use Gui.broadcast_callback to fan out periodic refreshes to all connected users. With this wiring, your KPIs and tables update predictably both on demand and on schedule, across every page and every session.
The article is based on a question from StackOverflow by user3219244 and an answer by Zaccheus Sia.