State Management
Signal System
StateFlow's signal system provides the exclusive pathway for state modifications, ensuring that every state change request receives explicit feedback. This architecture eliminates silent failures and provides comprehensive tracking of all state transitions.
Signal Architecture
The signal system operates on a fundamental principle that distinguishes StateFlow from traditional state management approaches. Rather than allowing direct state mutations, all state changes must flow through signals, which act as validated commands with guaranteed handling and explicit results.
Signal as Commands
Signals represent intent to change state rather than direct mutations. This distinction is crucial for maintaining system integrity. When you dispatch a signal, you are requesting a state change, not demanding it. The system evaluates this request against current state and business rules before deciding how to proceed.
// Traditional approach - direct mutation with no feedback
state.volume = 0.8; // What if this fails? What if it's invalid?
// StateFlow approach - signal with guaranteed feedback
const result = dispatch(app, signals.setVolume({ level: 0.8 }));
if (result.kind === ResultKind.Rejected) {
console.error(`Volume change rejected: ${result.message}`);
}
Type-Safe Signal Parameters
Signals leverage TypeScript's type system to ensure parameter correctness at compile time. This prevents a entire class of runtime errors related to incorrect signal data.
const authSignals = {
login: defineSignal<{
username: string;
password: string;
rememberMe?: boolean;
}>("login"),
logout: defineSignal("logout"),
updateProfile: defineSignal<{
displayName?: string;
email?: string;
avatarUrl?: string;
}>("updateProfile")
};
// Type checking prevents errors
dispatch(app, authSignals.login({
username: "user@example.com"
// Error: Property 'password' is missing
}));
Guaranteed Handling
Every signal dispatched through StateFlow receives explicit handling, even if that handling is to ignore the signal. This guarantee eliminates the uncertainty of whether a state change request was processed.
Explicit Results
StateFlow's Result type system ensures that every signal dispatch provides clear feedback about what occurred:
function performAction() {
const result = dispatch(app, signals.play());
// Exhaustive handling ensures no case is missed
switch (result.kind) {
case ResultKind.OK:
updateUI("Playing");
break;
case ResultKind.Ignored:
// Signal was not applicable to current state
console.log("Already playing or signal not handled");
break;
case ResultKind.Rejected:
showError(`Cannot play: ${result.message}`);
break;
case ResultKind.Error:
logError(result.error);
showError("An unexpected error occurred");
break;
case ResultKind.InTransition:
showLoading();
handleAsyncResult(result);
break;
}
}
Signal Routing
StateFlow automatically routes signals to the appropriate handlers based on current state. This routing is deterministic and type-safe:
defineFlow(mediaState.paused, {
play: (state) => mediaState.playing(state),
stop: (state) => mediaState.stopped({ ...state, position: 0 })
// 'pause' signal is not defined - will be Ignored
});
defineFlow(mediaState.playing, {
pause: (state) => mediaState.paused(state),
stop: (state) => mediaState.stopped({ ...state, position: 0 })
// 'play' signal is not defined - will be Ignored
});
// Signal routing based on current state
dispatch(app, signals.play()); // Handled if paused, ignored if playing
dispatch(app, signals.pause()); // Handled if playing, ignored if paused
Comprehensive Feedback
The signal system provides detailed feedback mechanisms that support debugging, monitoring, and error recovery.
Result Metadata
Results carry contextual information about signal processing:
const result = dispatch(app, signals.connect())
.withSignal(signals.connect())
.withHandlerName("handleConnect")
.withStateUpdating("connection.disconnected->connection.connecting");
// Results can be inspected for debugging
if (result.kind === ResultKind.Error) {
console.error("Signal:", result.signal);
console.error("Handler:", result.handler);
console.error("State transition:", result.stateUpdate);
console.error("Error:", result.error);
console.error("Stack:", result.stacktrace);
}
Result Merging
When multiple states handle a signal, their results are merged according to strict priority rules:
// Consider an application with multiple states
const app = {
auth: { /* ... */ },
data: { /* ... */ },
ui: { /* ... */ }
};
// When a signal affects multiple states
const result = dispatch(app, signals.logout());
// Results are merged with priority:
// 1. Error - any error stops processing
// 2. Rejected - validation failures take precedence
// 3. InTransition - async operations are awaited
// 4. OK - successful handling
// 5. Ignored - lowest priority
// The final result reflects the highest priority outcome
Asynchronous Signal Handling
Signals can trigger asynchronous operations while maintaining feedback guarantees:
applyFlow(app, [dataState], (sm) => {
sm.addEnterHandler(dataState.loading, (state) => {
return Result.transition(async () => {
try {
const data = await fetchData(state.query);
// Dispatch follow-up signal
return dispatch(app, signals.dataLoaded({ data }));
} catch (error) {
return Result.error(error);
}
}, 10000); // 10 second timeout
});
});
// Async feedback handling
async function loadData(query: string) {
const result = dispatch(app, signals.startLoading({ query }));
if (result.kind === ResultKind.InTransition) {
showLoadingSpinner();
const finalResult = await result.done();
hideLoadingSpinner();
if (finalResult.kind === ResultKind.OK) {
displayData();
} else {
showError(finalResult);
}
}
}
Signal Validation
The signal system supports comprehensive validation at multiple levels to ensure system integrity.
Parameter Validation
Signal handlers can validate parameters before processing:
defineFlow(formState.editing, {
submit: (state, signal) => {
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(signal.email)) {
return Result.reject("Invalid email format");
}
// Password strength validation
if (signal.password.length < 8) {
return Result.reject("Password must be at least 8 characters");
}
// Business rule validation
if (state.submitCount >= 3) {
return Result.reject("Too many submission attempts");
}
return formState.submitting({
...state,
formData: signal,
submitCount: state.submitCount + 1
});
}
});
Cross-State Validation
Signals can be validated against the broader application context:
defineFlow(orderState.draft, {
submit: (state, signal, context) => {
// Validate against user state
if (!context.user.isVerified) {
return Result.reject("Account must be verified to place orders");
}
// Validate against inventory state
const available = context.inventory[state.productId];
if (available < state.quantity) {
return Result.reject(`Only ${available} items in stock`);
}
// Validate against payment state
if (context.payment.method === null) {
return Result.reject("Payment method required");
}
return orderState.processing(state);
}
});
Signal Patterns
Common patterns emerge when working with StateFlow's signal system that promote clean, maintainable code.
Command and Query Separation
Signals should represent commands (state changes) rather than queries (state reads):
// ✅ Good - signals for state changes
const signals = {
startRecording: defineSignal("startRecording"),
stopRecording: defineSignal("stopRecording"),
setQuality: defineSignal<{ quality: 'high' | 'medium' | 'low' }>("setQuality")
};
// ❌ Avoid - signals for queries
const badSignals = {
getRecordingStatus: defineSignal("getRecordingStatus"), // Use direct state access
isRecording: defineSignal("isRecording") // Use state observation
};
Signal Composition
Complex operations can be composed from simpler signals:
async function saveAndClose(data: FormData) {
// Compose multiple signals for complex operations
const saveResult = await dispatch(app, signals.save({ data })).done();
if (saveResult.kind === ResultKind.OK) {
const closeResult = dispatch(app, signals.close());
return closeResult;
}
return saveResult;
}
Error Recovery Signals
Design signals that support error recovery flows:
const recoverySignals = {
retry: defineSignal<{ attemptNumber: number }>("retry"),
fallback: defineSignal<{ useCache: boolean }>("fallback"),
reset: defineSignal("reset")
};
defineFlow(apiState.error, {
retry: (state, signal) => {
if (signal.attemptNumber > 3) {
return Result.reject("Maximum retry attempts exceeded");
}
return apiState.fetching({ ...state, attempt: signal.attemptNumber });
},
fallback: (state, signal) => {
if (signal.useCache && state.cachedData) {
return apiState.ready({ ...state, data: state.cachedData });
}
return Result.reject("No cached data available");
},
reset: () => apiState.idle(initialApiState)
});
Through these mechanisms, StateFlow's signal system provides a robust foundation for state management that guarantees handling, provides comprehensive feedback, and maintains system integrity throughout the application lifecycle.