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:
Single Source of Truth - Application state lives in a single store
State is Read-Only - State can only be changed by dispatching actions
Changes via Pure Functions - Reducers are pure functions that transform state
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¶
Keep Reducers Pure - No side effects, API calls, or random values
Normalize State - Store data in normalized form (keyed by ID)
Use Action Creators - Encapsulate action creation logic
Middleware for Side Effects - Use middleware for async operations
Selective Updates - Only update UI when relevant state changes
Single Store - Use one store per application
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}"))