State Management Guide

Qt Framework includes a Redux-inspired state management system for predictable application state handling. This guide covers actions, reducers, middleware, and the store.

Overview

The state management system follows these principles:

  1. Single Source of Truth - Application state lives in a single store

  2. State is Read-Only - State can only be changed by dispatching actions

  3. Changes via Pure Functions - Reducers are pure functions that transform state

  4. Middleware for Side Effects - Async operations and side effects handled by middleware

Quick Start

Basic Setup

from qtframework.state import Store, create_action, create_reducer

# Define initial state
initial_state = {
    "counter": 0,
    "user": None,
    "items": []
}

# Create actions
increment = create_action("INCREMENT")
decrement = create_action("DECREMENT")
set_user = create_action("SET_USER")

# Create reducer
def counter_reducer(state, action):
    if action.type == "INCREMENT":
        return {**state, "counter": state["counter"] + 1}
    elif action.type == "DECREMENT":
        return {**state, "counter": state["counter"] - 1}
    elif action.type == "SET_USER":
        return {**state, "user": action.payload}
    return state

# Create store
store = Store(initial_state, counter_reducer)

# Dispatch actions
store.dispatch(increment())
store.dispatch(increment())
print(store.get_state()["counter"])  # 2

store.dispatch(set_user({"name": "Alice", "id": 1}))
print(store.get_state()["user"])  # {"name": "Alice", "id": 1}

Actions

Actions are payloads of information that send data to your store.

Creating Actions

from qtframework.state import Action, create_action

# Simple action
logout = create_action("LOGOUT")
store.dispatch(logout())

# Action with payload
add_item = create_action("ADD_ITEM")
store.dispatch(add_item({"id": 1, "name": "Item 1"}))

# Action with multiple parameters
update_user = create_action("UPDATE_USER")
store.dispatch(update_user({
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com"
}))

Action Creators

For complex actions, create action creator functions:

def fetch_user_success(user_data):
    return Action(
        type="FETCH_USER_SUCCESS",
        payload=user_data,
        meta={"timestamp": time.time()}
    )

def fetch_user_error(error):
    return Action(
        type="FETCH_USER_ERROR",
        payload=error,
        error=True
    )

Async Actions

For async operations, use middleware (see Middleware section):

async def fetch_user(user_id):
    return Action(
        type="FETCH_USER",
        payload=user_id,
        meta={"async": True}
    )

Reducers

Reducers specify how the application’s state changes in response to actions.

Basic Reducer

def app_reducer(state, action):
    """Main application reducer."""
    if action.type == "SET_THEME":
        return {**state, "theme": action.payload}

    elif action.type == "SET_LANGUAGE":
        return {**state, "language": action.payload}

    elif action.type == "TOGGLE_SIDEBAR":
        return {**state, "sidebar_open": not state.get("sidebar_open", True)}

    return state

Combining Reducers

Split your reducer into smaller functions:

from qtframework.state import combine_reducers

def user_reducer(state, action):
    """Handle user-related state."""
    if action.type == "SET_USER":
        return action.payload
    elif action.type == "LOGOUT":
        return None
    return state

def items_reducer(state, action):
    """Handle items list."""
    if action.type == "ADD_ITEM":
        return [*state, action.payload]
    elif action.type == "REMOVE_ITEM":
        return [item for item in state if item["id"] != action.payload]
    elif action.type == "CLEAR_ITEMS":
        return []
    return state

def settings_reducer(state, action):
    """Handle application settings."""
    if action.type == "UPDATE_SETTING":
        return {**state, **action.payload}
    return state

# Combine into root reducer
root_reducer = combine_reducers({
    "user": user_reducer,
    "items": items_reducer,
    "settings": settings_reducer
})

# Initial state must match structure
initial_state = {
    "user": None,
    "items": [],
    "settings": {
        "theme": "light",
        "language": "en",
        "notifications": True
    }
}

store = Store(initial_state, root_reducer)

Immutable Updates

Always return new state objects, never mutate:

# ❌ BAD - Mutates state
def bad_reducer(state, action):
    if action.type == "ADD_ITEM":
        state["items"].append(action.payload)  # Mutates!
        return state

# ✅ GOOD - Returns new state
def good_reducer(state, action):
    if action.type == "ADD_ITEM":
        return {
            **state,
            "items": [*state["items"], action.payload]
        }
    return state

Store

The store holds your application state.

Creating a Store

from qtframework.state import Store

store = Store(
    initial_state=initial_state,
    reducer=root_reducer,
    middleware=[logger_middleware, async_middleware]
)

Store API

# Get current state
current_state = store.get_state()

# Dispatch an action
store.dispatch(add_item({"id": 1, "name": "Item"}))

# Subscribe to state changes
def on_state_change(state):
    print(f"State updated: {state}")

unsubscribe = store.subscribe(on_state_change)

# Unsubscribe when done
unsubscribe()

# Replace the reducer (useful for code splitting)
store.replace_reducer(new_reducer)

Middleware

Middleware provides extension points for custom behavior.

Logger Middleware

from qtframework.state import Middleware

class LoggerMiddleware(Middleware):
    """Logs all actions and state changes."""

    def process(self, store, action, next_dispatch):
        print(f"Dispatching: {action.type}")
        print(f"Payload: {action.payload}")

        # Call next middleware or reducer
        result = next_dispatch(store, action)

        print(f"New state: {store.get_state()}")
        return result

Async Middleware

Handle async operations:

import asyncio
from qtframework.state import Middleware, Action

class AsyncMiddleware(Middleware):
    """Handle async actions."""

    def process(self, store, action, next_dispatch):
        if not action.meta or not action.meta.get("async"):
            return next_dispatch(store, action)

        # Handle async action
        async def run_async():
            try:
                # Dispatch loading action
                store.dispatch(Action(
                    type=f"{action.type}_LOADING",
                    payload=True
                ))

                # Perform async operation (mock example)
                await asyncio.sleep(1)
                result = {"data": "fetched"}

                # Dispatch success action
                store.dispatch(Action(
                    type=f"{action.type}_SUCCESS",
                    payload=result
                ))

            except Exception as e:
                # Dispatch error action
                store.dispatch(Action(
                    type=f"{action.type}_ERROR",
                    payload=str(e),
                    error=True
                ))

        # Run async task
        asyncio.create_task(run_async())
        return action

Applying Middleware

store = Store(
    initial_state=initial_state,
    reducer=root_reducer,
    middleware=[
        LoggerMiddleware(),
        AsyncMiddleware(),
        # ... other middleware
    ]
)

Connecting to UI

Subscribe to State in Widgets

from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton

class CounterWidget(QWidget):
    def __init__(self, store):
        super().__init__()
        self.store = store

        # UI
        self.label = QLabel("0")
        self.inc_btn = QPushButton("+")
        self.dec_btn = QPushButton("-")

        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.inc_btn)
        layout.addWidget(self.dec_btn)
        self.setLayout(layout)

        # Connect buttons
        self.inc_btn.clicked.connect(lambda: store.dispatch(increment()))
        self.dec_btn.clicked.connect(lambda: store.dispatch(decrement()))

        # Subscribe to state
        self.store.subscribe(self.on_state_change)
        self.on_state_change(store.get_state())

    def on_state_change(self, state):
        """Update UI when state changes."""
        self.label.setText(str(state["counter"]))

Selective Subscriptions

Only update when specific state changes:

class UserWidget(QWidget):
    def __init__(self, store):
        super().__init__()
        self.store = store
        self.previous_user = None

        self.label = QLabel()
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        self.setLayout(layout)

        store.subscribe(self.on_state_change)
        self.on_state_change(store.get_state())

    def on_state_change(self, state):
        """Only update if user changed."""
        user = state.get("user")
        if user != self.previous_user:
            self.previous_user = user
            if user:
                self.label.setText(f"Hello, {user['name']}!")
            else:
                self.label.setText("Not logged in")

Complete Example

from qtframework.core import Application
from qtframework.state import Store, Action, Middleware, combine_reducers
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton

# Actions
increment = lambda: Action("INCREMENT")
decrement = lambda: Action("DECREMENT")
reset = lambda: Action("RESET")

# Reducers
def counter_reducer(state=0, action=None):
    if action.type == "INCREMENT":
        return state + 1
    elif action.type == "DECREMENT":
        return state - 1
    elif action.type == "RESET":
        return 0
    return state

def history_reducer(state=None, action=None):
    if state is None:
        state = []
    return [*state, action.type]

root_reducer = combine_reducers({
    "counter": counter_reducer,
    "history": history_reducer
})

# Middleware
class LoggerMiddleware(Middleware):
    def process(self, store, action, next_dispatch):
        print(f"Action: {action.type}")
        return next_dispatch(store, action)

# Create store
initial_state = {"counter": 0, "history": []}
store = Store(
    initial_state,
    root_reducer,
    middleware=[LoggerMiddleware()]
)

# UI
class CounterApp(QWidget):
    def __init__(self):
        super().__init__()
        self.counter_label = QLabel("0")
        self.history_label = QLabel("")

        inc_btn = QPushButton("Increment")
        dec_btn = QPushButton("Decrement")
        reset_btn = QPushButton("Reset")

        inc_btn.clicked.connect(lambda: store.dispatch(increment()))
        dec_btn.clicked.connect(lambda: store.dispatch(decrement()))
        reset_btn.clicked.connect(lambda: store.dispatch(reset()))

        layout = QVBoxLayout()
        layout.addWidget(self.counter_label)
        layout.addWidget(inc_btn)
        layout.addWidget(dec_btn)
        layout.addWidget(reset_btn)
        layout.addWidget(self.history_label)
        self.setLayout(layout)

        store.subscribe(self.update_ui)
        self.update_ui(store.get_state())

    def update_ui(self, state):
        self.counter_label.setText(f"Counter: {state['counter']}")
        self.history_label.setText(f"History: {', '.join(state['history'][-5:])}")

# Run app
app = Application()
window = CounterApp()
window.show()
app.exec()

Best Practices

  1. Keep Reducers Pure - No side effects, API calls, or random values

  2. Normalize State - Store data in normalized form (keyed by ID)

  3. Use Action Creators - Encapsulate action creation logic

  4. Middleware for Side Effects - Use middleware for async operations

  5. Selective Updates - Only update UI when relevant state changes

  6. Single Store - Use one store per application

  7. Immutable Updates - Never mutate state directly

Debugging

DevTools Integration

from qtframework.state import DevToolsMiddleware

store = Store(
    initial_state,
    root_reducer,
    middleware=[DevToolsMiddleware()]
)

State Inspection

# Print current state
print(store.get_state())

# Log all state changes
store.subscribe(lambda state: print(f"State: {state}"))