Files
michaelschiemer/docs/modules/state-manager.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

8.9 KiB

State Manager Module

Centralized, Reactive State Management for Client-Side State

The State Manager Module provides a centralized state management system similar to Redux or Vuex, with support for state persistence, cross-tab synchronization, and integration with LiveComponents.


Features

  • Reactive State Store - Subscribe to state changes and react automatically
  • State Persistence - Automatically save and restore state from localStorage or sessionStorage
  • Cross-Tab Synchronization - Keep state synchronized across browser tabs
  • Integration with LiveComponents - Seamless integration with LiveComponent state
  • Time-Travel Debugging - Debug state changes with history and time-travel
  • Middleware Support - Extend functionality with custom middleware

Quick Start

Basic Usage

import { StateManager } from './modules/state-manager/index.js';

// Create a state manager
const store = StateManager.create({
    initialState: {
        user: { name: '', email: '' },
        cart: { items: [], total: 0 },
        ui: { sidebarOpen: false }
    }
});

// Set state
store.set('user.name', 'John Doe');
store.set('cart.items', [{ id: 1, name: 'Product' }]);

// Get state
const userName = store.get('user.name');
const cartItems = store.get('cart.items', []);

// Subscribe to changes
const unsubscribe = store.subscribe('cart.items', (items) => {
    console.log('Cart items changed:', items);
    updateCartUI(items);
});

// Unsubscribe
unsubscribe();

Module System Integration

<!-- Enable global state manager -->
<script type="module">
    import { init } from './modules/state-manager/index.js';
    
    init({
        initialState: {
            user: {},
            cart: {}
        },
        persistence: {
            enabled: true,
            storage: 'localStorage',
            key: 'app-state'
        }
    });
</script>

<!-- Access globally -->
<script>
    window.StateManager.set('user.name', 'John');
    const name = window.StateManager.get('user.name');
</script>

API Reference

StateManager.create(config)

Create a new StateManager instance.

Parameters:

  • config.initialState - Initial state object
  • config.maxHistorySize - Maximum history size (default: 50)
  • config.enableHistory - Enable history for time-travel (default: false)
  • config.persistence.enabled - Enable state persistence (default: false)
  • config.persistence.storage - Storage type: 'localStorage' or 'sessionStorage' (default: 'localStorage')
  • config.persistence.key - Storage key (default: 'app-state')
  • config.persistence.paths - Array of paths to persist (empty = all)
  • config.sync.enabled - Enable cross-tab synchronization (default: false)

Example:

const store = StateManager.create({
    initialState: { user: {}, cart: {} },
    persistence: {
        enabled: true,
        storage: 'localStorage',
        paths: ['user', 'cart'] // Only persist these paths
    },
    sync: {
        enabled: true // Sync across tabs
    }
});

store.getState()

Get the entire state object.

Returns: Record<string, any>

store.get(path, defaultValue)

Get state at a specific path.

Parameters:

  • path - Dot-separated path (e.g., 'user.name')
  • defaultValue - Default value if path doesn't exist

Returns: any

Example:

const userName = store.get('user.name', 'Guest');
const cartTotal = store.get('cart.total', 0);

store.set(path, value)

Set state at a specific path.

Parameters:

  • path - Dot-separated path
  • value - Value to set

Example:

store.set('user.name', 'John Doe');
store.set('cart.items', [{ id: 1, name: 'Product' }]);

store.dispatch(action)

Dispatch an action (Redux-style).

Parameters:

  • action - Action object with type property, or a thunk function

Example:

// Simple action
store.dispatch({
    type: 'ADD_TO_CART',
    productId: 123,
    quantity: 1
});

// Thunk (async action)
store.dispatch(async (dispatch, getState) => {
    const response = await fetch('/api/products');
    const products = await response.json();
    dispatch({ type: 'SET_PRODUCTS', products });
});

store.subscribe(path, callback)

Subscribe to state changes at a specific path.

Parameters:

  • path - Dot-separated path, or '*' for all changes
  • callback - Callback function: (newValue, oldValue, path) => void

Returns: Unsubscribe function

Example:

const unsubscribe = store.subscribe('cart.items', (items, oldItems, path) => {
    console.log(`Cart items changed at ${path}:`, items);
    updateCartUI(items);
});

// Later...
unsubscribe();

store.subscribeAll(callback)

Subscribe to all state changes.

Parameters:

  • callback - Callback function: (state) => void

Returns: Unsubscribe function

store.use(middleware)

Add middleware to the state manager.

Parameters:

  • middleware - Middleware function: (action, getState) => action | null

Example:

// Logging middleware
store.use((action, getState) => {
    console.log('Action:', action);
    console.log('Current state:', getState());
    return action; // Return action to continue, or null to block
});

// Validation middleware
store.use((action, getState) => {
    if (action.type === 'SET' && action.path === 'user.email') {
        if (!isValidEmail(action.value)) {
            console.error('Invalid email');
            return null; // Block the action
        }
    }
    return action;
});

store.getHistory()

Get action history for time-travel debugging.

Returns: Array<HistoryPoint>

store.timeTravel(index)

Time-travel to a specific history point.

Parameters:

  • index - History index

store.reset()

Reset state to initial state.

store.destroy()

Destroy the state manager and clean up resources.


Integration with LiveComponents

import { StateManager } from './modules/state-manager/index.js';
import { LiveComponentManager } from './modules/livecomponent/index.js';

const store = StateManager.create();
const lcManager = LiveComponentManager.getInstance();

// Sync LiveComponent state with StateManager
lcManager.on('component:state-updated', (componentId, state) => {
    store.set(`livecomponents.${componentId}`, state);
});

// Update LiveComponent from StateManager
store.subscribe('livecomponents', (state) => {
    Object.entries(state).forEach(([componentId, componentState]) => {
        lcManager.updateComponentState(componentId, componentState);
    });
});

Use Cases

User Preferences

const store = StateManager.create({
    initialState: {
        preferences: {
            theme: 'light',
            language: 'en',
            notifications: true
        }
    },
    persistence: {
        enabled: true,
        storage: 'localStorage',
        paths: ['preferences']
    }
});

// Save preference
store.set('preferences.theme', 'dark');

// Load preference
const theme = store.get('preferences.theme', 'light');

Shopping Cart

const store = StateManager.create({
    initialState: {
        cart: {
            items: [],
            total: 0
        }
    },
    persistence: {
        enabled: true,
        storage: 'sessionStorage',
        paths: ['cart']
    },
    sync: {
        enabled: true // Sync cart across tabs
    }
});

// Add item
store.set('cart.items', [
    ...store.get('cart.items', []),
    { id: 1, name: 'Product', price: 99.99 }
]);

// Calculate total
store.subscribe('cart.items', (items) => {
    const total = items.reduce((sum, item) => sum + item.price, 0);
    store.set('cart.total', total);
});

UI State

const store = StateManager.create({
    initialState: {
        ui: {
            sidebarOpen: false,
            modalOpen: false,
            activeTab: 'home'
        }
    }
});

// Toggle sidebar
store.set('ui.sidebarOpen', !store.get('ui.sidebarOpen'));

// Subscribe to UI changes
store.subscribe('ui', (uiState) => {
    updateUI(uiState);
});

Best Practices

  1. Use Scoped State Managers - Create separate state managers for different features
  2. Persist Only Necessary Data - Use paths to limit what gets persisted
  3. Use Middleware for Cross-Cutting Concerns - Logging, validation, etc.
  4. Subscribe Selectively - Only subscribe to paths you need
  5. Clean Up Subscriptions - Always call unsubscribe when done

Performance Considerations

  • State updates are synchronous and immediate
  • Subscriptions are called synchronously (be careful with expensive operations)
  • Persistence is debounced internally
  • Cross-tab sync uses BroadcastChannel (efficient)

Browser Support

  • Chrome/Edge: 38+
  • Firefox: 38+
  • Safari: 15.4+
  • Mobile: iOS 15.4+, Android Chrome 38+

Required Features:

  • ES2020 JavaScript
  • BroadcastChannel (for cross-tab sync)
  • localStorage/sessionStorage (for persistence)

Next: Validation Module