State Management
State Visibility
StateFlow provides comprehensive visibility into your application's state through built-in string representations, extensive logging capabilities, and observable state changes. This transparency is essential for debugging complex applications and understanding system behavior at runtime.
The Visibility Challenge
Complex applications often suffer from opaque state that makes debugging difficult. Developers struggle to understand what state the application is in, how it got there, and why certain behaviors occur. StateFlow addresses these challenges through systematic visibility features built into the core architecture.
Traditional debugging approaches often require extensive console logging, debugger breakpoints, or external tooling. StateFlow integrates visibility directly into the state management system, providing immediate insight without additional instrumentation. This built-in transparency reduces debugging time and helps developers understand application behavior more quickly.
String Representations
Every state in StateFlow can provide a human-readable string representation, making it immediately clear what state the application is in during debugging sessions.
Defining String Representations
String representations are defined as part of the state definition using the stringRepr
method. This method receives the current state instance and returns a formatted string that captures the essential information about that state.
const connectionState = defineState<{
url: string;
attemptCount: number;
lastError?: Error;
connectedAt?: number;
}>()
.name("connection")
.variant("disconnected", true)
.variant("connecting")
.variant("connected")
.variant("failed")
.stringRepr(state => {
const baseInfo = `${state.url} (attempts: ${state.attemptCount})`;
if (state.lastError) {
return `${baseInfo} - Error: ${state.lastError.message}`;
}
if (state.connectedAt) {
const duration = Date.now() - state.connectedAt;
return `${baseInfo} - Connected for ${Math.round(duration / 1000)}s`;
}
return baseInfo;
})
.build();
Automatic String Conversion
StateFlow automatically uses these representations when converting states to strings, making debugging output immediately useful:
const state = connectionState.connecting({
url: "wss://api.example.com",
attemptCount: 2
});
console.log(String(state));
// Output: "connection.connecting(wss://api.example.com (attempts: 2))"
// The format includes:
// - State definition name: "connection"
// - Current variant: "connecting"
// - Custom string representation in parentheses
Value Truncation
StateFlow automatically truncates long values in string representations to maintain readability:
const dataState = defineState<{
payload: string;
items: any[];
metadata: Record<string, any>;
}>()
.name("data")
.variant("loaded")
.build();
const state = dataState.loaded({
payload: "x".repeat(50), // Long string
items: new Array(100).fill(0), // Large array
metadata: Object.fromEntries(
Array.from({ length: 20 }, (_, i) => [`key${i}`, i])
) // Large object
});
console.log(String(state));
// Output includes truncated values:
// - Strings > 15 chars: "xxxxxxxxxxxxxxx..."
// - Arrays > 3 items: "[array: 100 items]"
// - Objects > 5 props: "[object: 20 props]"
Comprehensive Logging
StateFlow provides detailed logging for every state transition, signal dispatch, and handler execution. This logging system captures the complete flow of state changes through your application.
Log Entry Structure
Each state flow operation generates a comprehensive log entry containing all relevant information about the operation:
interface StateFlowLogEntry {
// Operation identification
flowName: string; // Name of the flow (e.g., "mediaPlayer")
signal: string; // Signal that triggered the operation
startTime: number; // Timestamp when operation began
duration?: number; // Duration for async operations
// State information
finalStates: Record<string, string>; // All states after operation
stateChanges: Array<{ // Detailed state transitions
stateName: string;
oldState: StateInstance;
newState: StateInstance;
}>;
// Handler execution details
handlerResults: Array<{
type: "enter" | "exit" | "update" | "rollback";
handlerName: string;
stateName: string;
result: string;
}>;
// Observer notifications
observers: Array<{
observerName: string;
stateName: string;
needObserve: boolean;
}>;
// Result information
finalResult: string; // Final operation result
isAsync: boolean; // Whether operation was asynchronous
stacktrace: Error | null; // Stack trace for errors
}
Custom Log Handlers
StateFlow allows custom log handlers to integrate with your preferred logging infrastructure:
const customLogHandler: StateFlowLogHandler = (entry) => {
// Send to external logging service
if (entry.finalResult.includes("Error")) {
errorReporter.logError({
message: entry.message,
context: {
signal: entry.signal,
states: entry.finalStates,
duration: entry.duration
},
stackTrace: entry.stacktrace
});
}
// Custom formatting for development
if (process.env.NODE_ENV === "development") {
console.group(`π ${entry.signal} β ${entry.finalResult}`);
entry.stateChanges.forEach(change => {
console.log(`π ${change.stateName}:`,
String(change.oldState), "β", String(change.newState)
);
});
console.groupEnd();
}
};
// Apply custom logging
applyFlow(app, [mediaState], (sm) => {
// Handler setup
}, {
logHandlers: [customLogHandler]
});
Structured Logging Output
The default console log handler provides structured output that groups related information:
[SF/mediaPlayer] play{} - OK
State: media.paused(position=0/duration=180) => media.playing(position=0/duration=180)
enter startPlayback() => OK
observed by updateUIState() => true
Final States:
media: media.playing(position=0/duration=180)
buffer: buffer.active(level=0.2)
State Observation
StateFlow's observation system provides real-time visibility into state changes, enabling reactive UI updates and debugging insights.
Basic Observation
Observers are notified whenever specified state variants change:
const observer = observe(
app,
[connectionState.connected, connectionState.failed],
(state) => {
console.log(`Connection state changed: ${String(state)}`);
// Update debugging UI
debugPanel.updateConnectionStatus({
variant: state[Symbol.toStringTag],
data: state,
timestamp: Date.now()
});
}
);
// Cleanup when debugging session ends
observer[Symbol.dispose]();
Filtered Observation
Custom comparison functions enable fine-grained observation of specific state changes:
// Only notify when specific fields change
observe(
app,
[dataState.loaded],
(state) => {
console.log(`Page changed to: ${state.currentPage}`);
},
(previous, current) => previous.currentPage !== current.currentPage
);
// Monitor error accumulation
observe(
app,
[apiState.retrying],
(state) => {
if (state.errorCount > 3) {
console.warn("Multiple API failures detected", state.errors);
}
},
(prev, curr) => curr.errorCount > prev.errorCount
);
Debugging with Observers
Observers can be strategically placed to understand application flow:
function enableDebugMode() {
// Monitor all state transitions
const allStates = [
authState.loggedOut, authState.loggingIn, authState.loggedIn,
dataState.idle, dataState.loading, dataState.loaded, dataState.error
];
const debugObserver = observe(
app,
allStates,
(state) => {
const stateInfo = {
timestamp: new Date().toISOString(),
state: String(state),
variant: state[Symbol.toStringTag],
data: JSON.stringify(state, null, 2)
};
// Update debug panel
debugPanel.addStateTransition(stateInfo);
// Log to console with formatting
console.log(
`%c[${stateInfo.timestamp}] ${stateInfo.state}`,
'color: blue; font-weight: bold'
);
}
);
return debugObserver;
}
Runtime Introspection
StateFlow provides mechanisms for inspecting application state at runtime, useful for debugging tools and development interfaces.
State Inspection
Current state can be inspected without triggering changes:
function inspectApplicationState(app: any) {
const stateInfo = {};
// Extract all current states
for (const key in app) {
const value = app[key];
if (isState(value)) {
stateInfo[key] = {
variant: value[Symbol.toStringTag],
string: String(value),
data: { ...value } // Shallow copy of state data
};
}
}
return stateInfo;
}
// Use in debugging console
window.debugStateFlow = () => {
const info = inspectApplicationState(app);
console.table(info);
return info;
};
Signal History Tracking
Track signal dispatch history for debugging:
class SignalHistory {
private history: Array<{
signal: string;
timestamp: number;
result: ResultKind;
duration?: number;
}> = [];
createLogHandler(): StateFlowLogHandler {
return (entry) => {
this.history.push({
signal: entry.signal,
timestamp: entry.startTime,
result: this.parseResult(entry.finalResult),
duration: entry.duration
});
// Keep last 100 entries
if (this.history.length > 100) {
this.history.shift();
}
};
}
getHistory() {
return this.history;
}
findPattern(pattern: RegExp) {
return this.history.filter(entry =>
pattern.test(entry.signal)
);
}
private parseResult(result: string): ResultKind {
if (result.includes("OK")) return ResultKind.OK;
if (result.includes("Rejected")) return ResultKind.Rejected;
if (result.includes("Error")) return ResultKind.Error;
if (result.includes("Ignored")) return ResultKind.Ignored;
return ResultKind.InTransition;
}
}
Development Tools Integration
StateFlow's visibility features integrate well with development tools and debugging workflows.
Browser DevTools Integration
Create custom formatters for browser DevTools:
// Enable custom formatters in Chrome DevTools settings
window.devtoolsFormatters = [{
header: (obj) => {
if (isState(obj)) {
return ["div", { style: "color: #880088" },
`StateFlow: ${String(obj)}`
];
}
return null;
},
hasBody: (obj) => isState(obj),
body: (obj) => {
const props = Object.entries(obj)
.filter(([key]) => !key.startsWith(Symbol.toStringTag))
.map(([key, value]) =>
["div", { style: "margin-left: 20px" },
["span", { style: "color: #0066cc" }, key + ": "],
["span", {}, JSON.stringify(value)]
]
);
return ["div", {}, ...props];
}
}];
Time-Travel Debugging
Build time-travel debugging by tracking state history:
class StateHistory {
private snapshots: Array<{
timestamp: number;
signal: string;
states: Record<string, any>;
}> = [];
captureSnapshot(entry: StateFlowLogEntry) {
this.snapshots.push({
timestamp: entry.startTime,
signal: entry.signal,
states: { ...entry.finalStates }
});
}
replayToIndex(app: any, index: number) {
if (index < 0 || index >= this.snapshots.length) {
throw new Error("Invalid snapshot index");
}
const snapshot = this.snapshots[index];
console.log(`Replaying to: ${snapshot.signal} at ${new Date(snapshot.timestamp)}`);
// Note: This is a simplified example
// Real implementation would need to properly restore state instances
Object.assign(app, snapshot.states);
}
}
Through these visibility features, StateFlow ensures that developers can always understand what state their application is in, how it got there, and why specific behaviors occur. This transparency is fundamental to building and maintaining complex applications with confidence.