Introduction
Core Concepts
StateFlow uses four key concepts that work together to provide guaranteed state consistency and explicit feedback. Understanding these concepts is essential for effective StateFlow usage.
States: Immutable Data Snapshots
States represent your application's data at a specific point in time. They are immutable snapshots with variants representing different phases:
const userState = defineState<{
id: string;
email: string;
loginAttempts: number;
}>()
.name("user")
.variant("guest", true) // initial variant
.variant("authenticated")
.variant("locked")
.stringRepr(s => `${s.email} (${s.loginAttempts} attempts)`)
.build();
// Create state instance (immutable and frozen)
const guest = userState.guest({
id: '',
email: '',
loginAttempts: 0
});
// guest.email = 'modified'; // TypeError: Cannot assign to read only property
Key Points:
- States are frozen with
Object.freeze()
preventing mutations - Variants represent different phases (guest, authenticated, locked)
- Each variant shares the same data structure
- String representations aid debugging
Signals: Commands for State Changes
Signals are the only way to request state changes. They are type-safe command objects that trigger state transitions:
const userSignals = {
// Simple parameterless signal
logout: defineSignal("logout"),
// Parameterized signal with validation
login: defineSignal<{ email: string; password: string }>("login"),
// Complex signal with multiple parameters
updateProfile: defineSignal<{
name?: string;
email?: string;
avatar?: string
}>("updateProfile")
};
// Usage: Create signal instances
const loginSignal = userSignals.login({
email: 'user@example.com',
password: 'secret'
});
Key Points:
- Signals centralize all state change requests
- Type-safe parameters prevent runtime errors
- Immutable command objects with unique identifiers
Flows: Pure State Transition Logic
Flows define how state variants respond to signals. They are pure functions that return new states or Results:
defineFlow(userState.guest, {
login: (state, signal) => {
// Validation logic
if (!signal.email || !signal.password) {
return Result.reject('Email and password required');
}
// State transition
return userState.authenticated({
id: generateId(),
email: signal.email,
loginAttempts: 0
});
}
});
defineFlow(userState.authenticated, {
logout: (state) => userState.guest({
id: '',
email: '',
loginAttempts: state.loginAttempts
}),
updateProfile: (state, signal) => ({
...state,
email: signal.email || state.email
})
});
Flow Rules:
- Pure functions only (no side effects)
- Must return synchronously
- Each variant has one flow definition
- Unhandled signals are ignored
Results: Explicit Operation Feedback
Every signal dispatch returns a Result that explicitly indicates what happened, eliminating silent failures:
// Modern pattern - use .expect() and .done()
try {
await dispatch(app, userSignals.login({ email, password }))
.expect(ResultKind.OK)
.done();
console.log('Login successful');
} catch (error) {
console.error('Login failed:', error);
}
// Result types returned by flows:
// Result.ok() - Success
// Result.reject('message') - Validation failed
// Result.error(error) - Exception occurred
// Result.ignore('reason') - Signal not applicable
// Result.transition(asyncFn, timeout) - Async operation (state handlers only)
Key Points:
- Results provide explicit feedback for every operation
- Use
.expect()
to validate expected result types - Use
.done()
to handle both sync and async operations
Application Integration
Connect StateFlow to your application using applyFlow
for side effects and resource management:
const app = {
user: { id: '', email: '', loginAttempts: 0 },
connection: { status: 'disconnected', url: '' }
};
applyFlow(app, [userState, connectionState], (sm) => {
// Side effects when entering states
sm.addEnterHandler(userState.authenticated, (state) => {
localStorage.setItem('userId', state.id);
return Result.ok();
});
// Async operations with timeout
sm.addEnterHandler(connectionState.connecting, (state) => {
return Result.transition(async () => {
await connectToServer(state.url);
return Result.ok();
}, 5000);
});
// Cleanup when leaving states
sm.addExitHandler(userState.authenticated, (state) => {
localStorage.removeItem('userId');
return Result.ok();
});
});
Handler Types:
addEnterHandler
- Called when entering a stateaddExitHandler
- Called when leaving a stateaddUpdateHandler
- Called when state data changesaddRollbackHandler
- Called when transitions fail
Observability: State Monitoring
StateFlow provides built-in mechanisms for monitoring state changes:
// Observe specific state variants
const subscription = observe(
app,
[connectionState.connected, connectionState.failed],
(state) => {
updateUIConnectionStatus(state);
}
);
// Custom comparison for fine-grained updates
observe(
app,
[connectionState.connecting],
(state) => updateRetryCounter(state.retryCount),
(prev, curr) => prev.retryCount !== curr.retryCount
);
// Cleanup when done
subscription[Symbol.dispose]();
These concepts work together to create a state management system that is predictable, debuggable, and type-safe, while providing explicit feedback for every operation.