Skip to main content

State Management Deep Dive

Advanced patterns and implementation details of the state management system.

Architecture​

StateManager Implementation​

Core Pattern​

class StateManager {
#state = new Map();
#subscribers = new Map();
#persistence = null;

constructor() {
this.#loadInitialState();
}

set(key, value) {
const oldValue = this.#state.get(key);

// Validate if validator exists
if (this.#validators.has(key)) {
const valid = this.#validators.get(key)(value);
if (!valid) throw new Error(`Invalid value for ${key}`);
}

// Update state
this.#state.set(key, value);

// Notify subscribers
this.#notify(key, value, oldValue);

// Persist if configured
if (this.#shouldPersist(key)) {
this.#persist(key, value);
}
}

get(key) {
return this.#state.get(key);
}

subscribe(key, callback) {
if (!this.#subscribers.has(key)) {
this.#subscribers.set(key, new Set());
}
this.#subscribers.get(key).add(callback);

// Return unsubscribe function
return () => {
this.#subscribers.get(key).delete(callback);
};
}

#notify(key, newValue, oldValue) {
const subscribers = this.#subscribers.get(key);
if (subscribers) {
subscribers.forEach(callback => {
callback(newValue, oldValue);
});
}
}
}

Singleton Pattern​

// Ensure single instance
let instance = null;

export function getStateManager() {
if (!instance) {
instance = new StateManager();
}
return instance;
}

// Export for convenience
export const stateManager = getStateManager();

Persistence Strategies​

LocalStorage Strategy​

class LocalStoragePersistence {
constructor(prefix = 'fitfileviewer_') {
this.prefix = prefix;
}

save(key, value) {
const storageKey = this.prefix + key;
const serialized = JSON.stringify(value);
localStorage.setItem(storageKey, serialized);
}

load(key) {
const storageKey = this.prefix + key;
const serialized = localStorage.getItem(storageKey);
return serialized ? JSON.parse(serialized) : undefined;
}

remove(key) {
const storageKey = this.prefix + key;
localStorage.removeItem(storageKey);
}
}

Electron Store Strategy​

class ElectronStorePersistence {
constructor(store) {
this.store = store;
}

save(key, value) {
this.store.set(key, value);
}

load(key) {
return this.store.get(key);
}

remove(key) {
this.store.delete(key);
}
}

Domain Managers​

ThemeManager​

class ThemeManager {
constructor(stateManager) {
this.stateManager = stateManager;
this.#initialize();
}

#initialize() {
// Load system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const savedTheme = this.stateManager.get('theme');

this.setTheme(savedTheme || (prefersDark ? 'dark' : 'light'));

// Listen for system changes
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!this.stateManager.get('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}

getTheme() {
return this.stateManager.get('theme');
}

setTheme(theme) {
this.stateManager.set('theme', theme);
document.documentElement.dataset.theme = theme;
}

toggleTheme() {
const current = this.getTheme();
this.setTheme(current === 'dark' ? 'light' : 'dark');
}
}

Subscription Patterns​

Component Integration​

// In a UI component
class MapComponent {
constructor() {
this.unsubscribers = [];
}

mount() {
// Subscribe to file changes
const unsub = stateManager.subscribe('currentFile', (file) => {
if (file) {
this.renderRoute(file.data.gpsPoints);
}
});
this.unsubscribers.push(unsub);

// Subscribe to theme changes
const themeUnsub = stateManager.subscribe('theme', (theme) => {
this.updateMapTheme(theme);
});
this.unsubscribers.push(themeUnsub);
}

unmount() {
// Clean up all subscriptions
this.unsubscribers.forEach(unsub => unsub());
this.unsubscribers = [];
}
}

Computed State​

// Derive state from other state
class ComputedState {
constructor(dependencies, compute) {
this.dependencies = dependencies;
this.compute = compute;
this.value = null;

this.#setup();
}

#setup() {
// Subscribe to all dependencies
this.dependencies.forEach(key => {
stateManager.subscribe(key, () => {
this.#recompute();
});
});

// Initial computation
this.#recompute();
}

#recompute() {
const values = this.dependencies.map(key =>
stateManager.get(key)
);
this.value = this.compute(...values);
}

get() {
return this.value;
}
}

// Usage
const formattedDistance = new ComputedState(
['currentFile', 'preferences'],
(file, prefs) => {
if (!file) return null;
return formatDistance(file.totalDistance, prefs.units);
}
);

Related: State Management API