Skip to content

State Machines

Reacton includes a built-in state machine primitive. Unlike a simple writable reacton where any value can be set at any time, a state machine enforces that state changes only happen through defined transitions. This makes complex workflows predictable, self-documenting, and debuggable.

Defining a State Machine

Use stateMachine<S, E>() to define a state machine with typed states and events:

dart
enum AuthState { loggedOut, loading, authenticated, error }
enum AuthEvent { login, logout, tokenExpired }

final authMachine = stateMachine<AuthState, AuthEvent>(
  initial: AuthState.loggedOut,
  transitions: {
    AuthState.loggedOut: {
      AuthEvent.login: (ctx) async {
        // Perform async login
        return AuthState.authenticated;
      },
    },
    AuthState.authenticated: {
      AuthEvent.logout: (ctx) => AuthState.loggedOut,
      AuthEvent.tokenExpired: (ctx) => AuthState.loggedOut,
    },
  },
  name: 'auth',
);

Factory Signature

dart
StateMachineReacton<S, E> stateMachine<S, E>({
  required S initial,
  required Map<S, Map<E, TransitionHandler<S>>> transitions,
  Map<E, TransitionGuard<S>>? guards,
  TransitionEffect<S>? onTransition,
  String? name,
  ReactonOptions<S>? options,
});
ParameterTypeDescription
initialSThe initial state.
transitionsMap<S, Map<E, TransitionHandler<S>>>Transition map: for each state, a map of events to handlers.
guardsMap<E, TransitionGuard<S>>?Optional guard functions that can block transitions.
onTransitionTransitionEffect<S>?Callback invoked after every successful transition.
nameString?Optional debug name.
optionsReactonOptions<S>?Standard reacton options (middleware, persistence, etc.).

Transition Map

The transition map is a nested Map<S, Map<E, TransitionHandler<S>>>:

  • Outer key -- the current state
  • Inner key -- the event
  • Value -- a handler that returns the next state
dart
transitions: {
  StateA: {
    EventX: (ctx) => StateB,         // sync transition
    EventY: (ctx) async => StateC,   // async transition
  },
  StateB: {
    EventZ: (ctx) => StateA,
  },
},

TransitionHandler

A transition handler receives a TransitionContext<S> and returns the next state, either synchronously or asynchronously:

dart
typedef TransitionHandler<S> = FutureOr<S> Function(TransitionContext<S> context);

TransitionContext

PropertyTypeDescription
currentStateSThe current state before the transition.
readReactonReader?Optional reader for accessing other reactons.

Sending Events

Use store.send() to send an event to a state machine:

dart
final newState = await store.send(authMachine, AuthEvent.login);
print(newState); // AuthState.authenticated

Signature

dart
Future<S> send<S, E>(StateMachineReacton<S, E> machine, E event);

send() throws StateError in three cases:

  1. No transition defined for the current state + event combination.
  2. Guard blocks the transition.
  3. Concurrent transition -- an async transition is already in progress.
dart
try {
  await store.send(authMachine, AuthEvent.login);
} on StateError catch (e) {
  print('Transition rejected: $e');
}

Reading State

machineState()

Read the current state of a state machine:

dart
final state = store.machineState(authMachine);
print(state); // AuthState.loggedOut

canSend()

Check if an event can be handled in the current state:

dart
if (store.canSend(authMachine, AuthEvent.login)) {
  await store.send(authMachine, AuthEvent.login);
}

Store Extension Summary

MethodSignatureDescription
sendFuture<S> send(machine, event)Send an event, triggering a transition. Returns the new state.
machineStateS machineState(machine)Read the current state.
canSendbool canSend(machine, event)Check if an event is valid for the current state.

Guards

Guards are predicate functions that can block a transition. If a guard returns false, the transition is rejected and send() throws a StateError.

dart
typedef TransitionGuard<S> = bool Function(S currentState);
dart
final authMachine = stateMachine<AuthState, AuthEvent>(
  initial: AuthState.loggedOut,
  transitions: {
    AuthState.loggedOut: {
      AuthEvent.login: (ctx) async => AuthState.authenticated,
    },
    AuthState.authenticated: {
      AuthEvent.logout: (ctx) => AuthState.loggedOut,
    },
  },
  guards: {
    // Only allow login when the network is available
    AuthEvent.login: (currentState) => networkService.isConnected,
  },
  name: 'auth',
);

TIP

Guards receive the current state. Use them to enforce preconditions that depend on external factors (network status, permissions, feature flags).

Side Effects (onTransition)

The onTransition callback is invoked after every successful transition. Use it for logging, analytics, or triggering side effects.

dart
typedef TransitionEffect<S> = void Function(S previousState, S newState);
dart
final authMachine = stateMachine<AuthState, AuthEvent>(
  initial: AuthState.loggedOut,
  transitions: { /* ... */ },
  onTransition: (previous, next) {
    analytics.track('auth_state_changed', {
      'from': previous.name,
      'to': next.name,
    });

    if (next == AuthState.loggedOut) {
      // Clear sensitive data
      store.set(userDataReacton, null);
    }
  },
  name: 'auth',
);

StateMachineReacton Properties

PropertyTypeDescription
initialSThe initial state value.
transitionsMap<S, Map<E, TransitionHandler<S>>>The full transition map.
guardsMap<E, TransitionGuard<S>>?Guard functions.
onTransitionTransitionEffect<S>?Post-transition callback.
stateReactonWritableReacton<S>The underlying writable reacton that holds the state value.
isTransitioningboolWhether an async transition is currently in progress.

Querying Valid Events

dart
// Get all events valid for a given state
final events = authMachine.validEvents(AuthState.loggedOut);
print(events); // {AuthEvent.login}

// Check if a specific event is valid
print(authMachine.canHandle(AuthState.loggedOut, AuthEvent.login)); // true
print(authMachine.canHandle(AuthState.loggedOut, AuthEvent.logout)); // false

State Diagram

Here is a Mermaid diagram of the authentication flow example:

mermaid
stateDiagram-v2
    [*] --> LoggedOut
    LoggedOut --> Authenticated : login
    Authenticated --> LoggedOut : logout
    Authenticated --> LoggedOut : tokenExpired

Complete Example: Multi-Step Wizard

dart
enum WizardState { info, address, payment, review, submitting, complete, error }
enum WizardEvent { next, back, submit, retry }

final wizardMachine = stateMachine<WizardState, WizardEvent>(
  initial: WizardState.info,
  transitions: {
    WizardState.info: {
      WizardEvent.next: (ctx) => WizardState.address,
    },
    WizardState.address: {
      WizardEvent.next: (ctx) => WizardState.payment,
      WizardEvent.back: (ctx) => WizardState.info,
    },
    WizardState.payment: {
      WizardEvent.next: (ctx) => WizardState.review,
      WizardEvent.back: (ctx) => WizardState.address,
    },
    WizardState.review: {
      WizardEvent.submit: (ctx) async {
        await api.submitOrder();
        return WizardState.complete;
      },
      WizardEvent.back: (ctx) => WizardState.payment,
    },
    WizardState.error: {
      WizardEvent.retry: (ctx) => WizardState.review,
    },
  },
  guards: {
    // Only allow submit when all fields are valid
    WizardEvent.submit: (state) => formValidator.isValid(),
  },
  onTransition: (prev, next) {
    analytics.track('wizard_step', {'from': prev.name, 'to': next.name});
  },
  name: 'wizard',
);

// Widget
class WizardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final state = store.machineState(wizardMachine);

    return Scaffold(
      body: switch (state) {
        WizardState.info => InfoForm(),
        WizardState.address => AddressForm(),
        WizardState.payment => PaymentForm(),
        WizardState.review => ReviewPage(),
        WizardState.submitting => Center(child: CircularProgressIndicator()),
        WizardState.complete => SuccessPage(),
        WizardState.error => ErrorPage(),
      },
      bottomNavigationBar: Row(
        children: [
          if (store.canSend(wizardMachine, WizardEvent.back))
            TextButton(
              onPressed: () => store.send(wizardMachine, WizardEvent.back),
              child: Text('Back'),
            ),
          Spacer(),
          if (store.canSend(wizardMachine, WizardEvent.next))
            ElevatedButton(
              onPressed: () => store.send(wizardMachine, WizardEvent.next),
              child: Text('Next'),
            ),
          if (store.canSend(wizardMachine, WizardEvent.submit))
            ElevatedButton(
              onPressed: () => store.send(wizardMachine, WizardEvent.submit),
              child: Text('Submit'),
            ),
        ],
      ),
    );
  }
}
mermaid
stateDiagram-v2
    [*] --> Info
    Info --> Address : next
    Address --> Payment : next
    Address --> Info : back
    Payment --> Review : next
    Payment --> Address : back
    Review --> Complete : submit
    Review --> Payment : back
    Error --> Review : retry

What's Next

  • Modules -- Group related reactons with lifecycle management
  • State Branching -- Preview state changes before committing
  • History -- Add undo/redo to state machines

Released under the MIT License.