Skip to main content

State Management

Reflex state patterns for Syntropy-Journals

Quick Reference

on_mount vs on_load

HookScopeTimingUse Case
on_loadPageBefore render, on route navigationData fetching, auth checks
on_mountComponentAfter DOM mountUI initialization, tab defaults, starting background loops

Route -> on_load Mapping

Routeon_load HandlersStates Initialized
/--
/login--
/dashboardCheckinState.load_checkinsCheckinState
/ai-chatChatState.load_chat_sessionsChatState
/catalogCatalogState.load_catalogCatalogState
/profileProfileState.load_profileProfileState
/settingsSettingsState.load_settingsSettingsState
/notificationsNotificationState.load_notificationsNotificationState
/partners/dashboardPartnerPortalState.on_loadPartnerPortalState

Modular State Chain Pattern

Each complex state is decomposed into a chain of mixins for maintainability:
_base -> _vars -> _ui_handlers -> _processes -> _on_load -> state
LayerResponsibilityExample
_base.pyImports, logger, type aliasesclass CheckinBase(rx.State)
_vars.pyState variables and computed varscheckins: list[dict], @rx.var
_ui_handlers.pySync UI event handlersModal open/close, tab switching
_processes.pyBackground business logic@rx.event(background=True) loaders
_on_load.pyPage on_load orchestrationCalls into _processes methods
state.pyFinal export, combines all layersclass CheckinState(OnLoadMixin)
Location: syntropy_journals/app/states/shared/checkin/ (and similar for other states)

Event Handler Types

Sync Events (@rx.event)

For fast UI updates that do not perform I/O:
@rx.event
def toggle_modal(self):
    self.show_modal = not self.show_modal
  • Runs on the main event loop
  • Has exclusive access to self (no async with self: needed)
  • Use for: toggling booleans, setting form values, pagination

Background Events (@rx.event(background=True))

For network calls, DB queries, LLM invocations:
@rx.event(background=True)
async def load_data(self):
    # State reads/writes MUST be inside `async with self:`
    async with self:
        if self._data_loaded:
            return
        self.is_loading = True

    # Heavy work OUTSIDE the lock
    auth_state = await self.get_state(AuthState)
    user_id = auth_state.user_id
    data = await asyncio.to_thread(_fetch_sync, user_id)

    # Quick state update back inside the lock
    async with self:
        self.data = data
        self.is_loading = False
        self._data_loaded = True
Rules:
  • async with self: is the state lock — keep it short
  • await self.get_state(OtherState) ONLY works inside background tasks
  • Do NOT await event handlers (they are async generators); use yield instead
  • Heavy work (DB, HTTP, LLM) goes outside the lock

yield vs return

# yield: emit intermediate events (e.g., trigger another state's handler)
@rx.event(background=True)
async def process_and_reload(self):
    await do_work()
    yield OtherState.reload_data  # Triggers OtherState handler

# return: early exit from event handler
@rx.event(background=True)
async def load_data(self):
    async with self:
        if self._data_loaded:
            return  # Skip -- already loaded

Loading Guard Pattern

All data-loading states use a _data_loaded flag to prevent duplicate fetches:
@rx.event(background=True)
async def load_data(self):
    async with self:
        if self._data_loaded:
            return
        self.is_loading = True

    try:
        auth_state = await self.get_state(AuthState)
        user_id = auth_state.user_id
        if not user_id:
            async with self:
                self.is_loading = False
            return

        data = await asyncio.to_thread(_fetch_sync, user_id)
        async with self:
            self.data = data
            self.is_loading = False
            self._data_loaded = True
    except Exception as e:
        logger.error("load_data failed: %s", e)
        async with self:
            self.is_loading = False

Force Reload Pattern

Reset the guard without clearing data (prevents UI flicker):
# From another state that needs to trigger a reload:
yield CheckinState.reset_data_loaded   # Clear guard, preserve data
yield CheckinState.load_checkins       # Will now re-execute

Periodic Background Sync Pattern

Used by CheckinState for CDC polling (call log sync):
@rx.event(background=True)
async def start_periodic_sync(self):
    """Start on component mount, stop on unmount."""
    async with self:
        if self.is_processing_background:
            return
        self.is_processing_background = True

    while True:
        async with self:
            if not self.is_processing_background:
                break

        processed = await asyncio.to_thread(process_batch)

        if processed > 0:
            async with self:
                # Reload data after new items processed
                self._data_loaded = False
            yield CheckinState.load_checkins

        await asyncio.sleep(poll_interval)

@rx.event
def stop_periodic_sync(self):
    self.is_processing_background = False
Lifecycle:
  1. on_mount -> start_periodic_sync() (background loop begins)
  2. Loop runs every poll_interval seconds
  3. on_unmount -> stop_periodic_sync() (sets flag, loop exits)

CDC Pipeline Flow

Change Data Capture for external data (e.g., call logs, DIET API):
Fetch from API -> Sync raw to DB -> Filter unprocessed -> LLM extract -> Save entries -> Reload UI
  1. Fetch: Pull new records from external API
  2. Sync: Upsert raw records into local DB (idempotent)
  3. Process: Claim unprocessed records, run through LLM extraction
  4. Save: Persist structured health entries (medications, food, symptoms)
  5. Reload: Reset _data_loaded flags, re-fetch dashboard data

Core State Classes

StateLocationPurpose
AuthStatestates/shared/auth.pyClerk authentication, user session
CheckinStatestates/shared/checkin/Health check-ins, CDC sync loop
ChatStatestates/chat/AI chat sessions, streaming, message history
CatalogStatestates/app/catalog/Product catalog browsing
NotificationStatestates/app/notification/Alerts, reminders
ProfileStatestates/app/profile/User health profile
SettingsStatestates/app/settings/User preferences
PartnerPortalStatestates/partner/portal.pyPartner portal tabs, store referral stats
NavbarStatestates/layout/navbar.pyTop nav UI state
SidebarStatestates/layout/sidebar.pySide nav UI state

Design Principles

  1. Composition over inheritance — States share patterns via copy-paste, not base classes. Reflex state inheritance creates parent-child coupling in the state tree.
  2. Thin states, fat functions — States delegate to functions/db_utils/ and functions/llm/ for business logic.
  3. Guard everything — Every background loader checks _data_loaded before fetching.
  4. User ID from AuthState — Always await self.get_state(AuthState) for the authenticated user’s ID.
  • Store Referral — Store referral attribution pipeline and portal integration
  • Lifecycle — Startup sequence and service registry
  • Debug Guide — Common issues and fixes