WORKING WITH STATE MACHINES IN ANGULAR

  • WHAT IS XSTATE, STATE MACHINES & STATECHARTS
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION
  • TAKEAWAY
 
  • WHAT IS A STATE MACHINE, A STATECHART, XSTATE
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 

Agenda

  • WHY THIS PRESENTATION
  • TAKEAWAY
 

The Journey to state machines

It might be the software entropy

The Journey to state machines

Out of the tar pit

The Journey to state machines

 Ben Moseley - Peter Marks

What is the complexity of a system?

The Journey to state machines

Lack of understanding

What we try to do most of the time is to read the source code and understand it

What is the complexity of a system?

The Journey to state machines

State

Control

Lack of understanding

What we try to do most of the time is to read the source code and understand it

The Journey to state machines

State

Control

What is the complexity of a system?

The Journey to state machines

Lack of understanding

What we try to do most of the time is to read the source code and understand it

In a team, we want all the participants to be able to speak the same language

XState

The Journey to state machines

THE JOURNEY TO STATE MACHINES

Lack of understanding

In a team, we want all the participants to be able to speak the same language

Stefanos Lignos

Front-end developer

github.com/stefanoslig

Not always dressed like this

Angular Athens

  • WHAT IS XSTATE, STATE MACHINES & STATECHARTS
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION
  • TAKEAWAY
 
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION

Agenda

Disclaimer: This is not a detailed explanation of XState, I couldn't do this in thirty minutes. It's a presentation about something that you might find useful/inspiring to apply in your project!
  • TAKEAWAY
 
  • WHAT IS A STATE MACHINE, A STATECHART, XSTATE

A library for creating finite state machines and statecharts

 

Xstate

 

What is

@DavidKPiano - Creator of XState

@DavidKPiano - Creator of XState

What is

 A FINITE STate machine

The life of a programmer as a finite state machine

  • David Harel - 1982
  • A common language for all the engineers in Israel Aircraft Industries
  • Finite State Machines extended with the notions of orthogonality and hierarchy

What is

 A statechart

What is

 A statechart

What is

 A statechart

Hierarchy
Orthogonality

The life of a programmer as a statechart 

Guarded Transitions
History nodes
Delayed events & transitions
  • WHAT IS XSTATE, STATE MACHINES & STATECHARTS
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION
  • TAKEAWAY
 

A second developer

who works in parallel

  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION

Agenda

  • TAKEAWAY
 
  • WHAT IS A STATE MACHINE, A STATECHART, XSTATE

CREATE A STATE MACHINE WITH XSTATE

A state machine for an authentication process

private authMachine = Machine<
    AuthMachineContext,
    AuthMachineSchema,
    AuthMachineEvent
  >(
    {
      id: 'auth',
      initial: 'unauthorized',
      states: {
        unauthorized: {},
        signin: {},
        signup: {},
        authorized: {},
        updating: {},
      },
    },
  1. Define the states

CREATE A STATE MACHINE WITH XSTATE

private authMachine = Machine<
    AuthMachineContext,
    AuthMachineSchema,
    AuthMachineEvent
  >(
    {
      id: 'auth',
      initial: 'unauthorized',
      states: {
        unauthorized: {
          on: {
            SIGNIN: 'signin',
            SIGNUP: 'signup',
          },
        },
        signin: {},
        signup: {},
        authorized: {
          on: {
            UPDATE_USER: 'updating',
            LOGOUT: { target: 'unauthorized' },
          },
        },
        updating: {},
      },
    },
  1. Define the states
  2. Create the events for the transitions

CREATE A STATE MACHINE WITH XSTATE

private ɵauthMachine = Machine<
    AuthMachineContext,
    AuthMachineSchema,
    AuthMachineEvent
  >(
    {
      id: 'auth',
      initial: 'unauthorized',
      states: {
        unauthorized: {
          on: {
            SIGNIN: 'signin',
            SIGNUP: 'signup',
          },
        },
        signin: {
          invoke: { src: 'signIn' },
          on: {
            SIGNIN_SUCCESS: { target: 'authorized' },
            SIGNIN_FAILURE: { target: 'unauthorized' },
          },
        },
        signup: {
          invoke: { src: 'signUp' },
          on: {
            SIGNUP_SUCCESS: { target: 'authorized' },
            SIGNUP_FAILURE: { target: 'unauthorized' },
          },
        },
        authorized: {
          on: {
            UPDATE_USER: 'updating',
            LOGOUT: { target: 'unauthorized' },
          },
        },
        updating: {},
      },
      {
      services: {
        signIn: (context, event: SignIn) =>
          this.authService
            .login({ email: event.username, password: event.password })
            .pipe(
              map((user) => SignInSuccess(user)),
              catchError((result) => of(SignInFail(result.error.errors)))
            ),
        signUp: (context, event: SignUp) =>
          this.authService
            .register({
              username: event.username,
              email: event.email,
              password: event.password,
            })
            .pipe(
              map((user) => SignUpSuccess(user)),
              catchError((result) => of(SignUpFail(result.error.errors)))
            ),
      },
    },
  1. Define the states
  2. Define the events for the unauthorized state
  3. Define the effects

CREATE A STATE MACHINE WITH XSTATE

CREATE A STATE MACHINE WITH XSTATE

Extended state/context: Data that can be potentially infinite

private ɵauthMachine = Machine<
    AuthMachineContext,
    AuthMachineSchema,
    AuthMachineEvent
  >(
    {
      id: 'auth',
      initial: 'unauthorized',
      context: {
        user: {
          email: '',
          token: '',
          username: '',
          bio: '',
          image: '',
        },
        errors: [],
      },
      states: {
        unauthorized: {
          on: {
            SIGNIN: 'signin',
            SIGNUP: 'signup',
          },
        },
        signin: {
          invoke: { src: 'signIn' },
          on: {
            SIGNIN_SUCCESS: { target: 'authorized' },
            SIGNIN_FAILURE: { target: 'unauthorized' },
          },
        },
        signup: {
          invoke: { src: 'signUp' },
          on: {
            SIGNUP_SUCCESS: { target: 'authorized' },
            SIGNUP_FAILURE: { target: 'unauthorized' },
          },
        },
        authorized: {
          on: {
            UPDATE_USER: 'updating',
            LOGOUT: { target: 'unauthorized' },
          },
        },
        updating: {},
      },
    },
  1. Define the states
  2. Define the events for the unauthorized state
  3. Define the effects
  4. Define the context

CREATE A STATE MACHINE WITH XSTATE

private ɵauthMachine = Machine<
    AuthMachineContext,
    AuthMachineSchema,
    AuthMachineEvent
  >(
    {
      id: 'auth',
      initial: 'unauthorized',
      context: {
        user: {
          email: '',
          token: '',
          username: '',
          bio: '',
          image: '',
        },
        errors: [],
      },
      states: {
        unauthorized: {
          on: {
            SIGNIN: 'signin',
            SIGNUP: 'signup',
          },
        },
        signin: {
          invoke: { src: 'signIn' },
          on: {
            SIGNIN_SUCCESS: { target: 'authorized', actions: 'assignUser' },
            SIGNIN_FAILURE: { target: 'unauthorized', actions: 'assignErrors' },
          },
        },
        signup: {
          invoke: { src: 'signUp' },
          on: {
            SIGNUP_SUCCESS: { target: 'authorized', actions: 'assignUser' },
            SIGNUP_FAILURE: { target: 'unauthorized', actions: 'assignErrors' },
          },
        },
        authorized: {
          on: {
            UPDATE_USER: 'updating',
            LOGOUT: { target: 'unauthorized', actions: 'logout' },
          },
        },
        updating: {},
      },
      {
      actions: {
        resetUser: assign<AuthMachineContext, AuthMachineEvent>(() => ({
          user: initialContext.user,
        })),
        resetErrors: assign<AuthMachineContext, AuthMachineEvent>(() => ({
          errors: initialContext.errors,
        })),
        goToHomePage: (ctx: AuthMachineContext, event: AuthMachineEvent) =>
          this.router.navigateByUrl(''),
        assignUser: assign<AuthMachineContext, AuthMachineEvent>(
          (ctx, event) => ({
            user: event.response.user,
          })
        ),
        assignErrors: assign<AuthMachineContext, AuthMachineEvent>(
          (_, event) => {
            const eventErrors = (event as SignInFail).errors;
            return {
              errors: Object.keys(eventErrors || {}).map(
                (key) => `${key} ${eventErrors[key]}`
              ),
            };
          }
        ),
        logout: () => {
          localStorage.setItem('authState', null);
          this.router.navigateByUrl('login');
        },
      },
      services: {
        signIn: (context, event: SignIn) =>
          this.authService
            .login({ email: event.username, password: event.password })
            .pipe(
              map((user) => new SignInSuccess(user)),
              catchError((result) => of(new SignInFail(result.error.errors)))
            ),
        signUp: (context, event: SignUp) =>
          this.authService
            .register({
              username: event.username,
              email: event.email,
              password: event.password,
            })
            .pipe(
              map((user) => new SignUpSuccess(user)),
              catchError((result) => of(new SignUpFail(result.error.errors)))
            ),
      },
    },
  1. Define the states
  2. Define the events for the unauthorized state
  3. Define the invoked effects
  4. Define the context
  5. Define the fire & forget effects

CREATE A STATE MACHINE WITH XSTATE

private ɵauthMachine = Machine<
    AuthMachineContext,
    AuthMachineSchema,
    AuthMachineEvent
  >(
    {
      id: 'auth',
      initial: 'unauthorized',
      context: {
        user: {
          email: '',
          token: '',
          username: '',
          bio: '',
          image: '',
        },
        errors: [],
      },
      states: {
        unauthorized: {
          on: {
            SIGNIN: 'signin',
            SIGNUP: 'signup',
          },
        },
        signin: {
          entry: 'resetErrors',
          invoke: { src: 'signIn' },
          on: {
            SIGNIN_SUCCESS: { target: 'authorized', actions: 'assignUser' },
            SIGNIN_FAILURE: { target: 'unauthorized', actions: 'assignErrors' },
          },
        },
        signup: {
          entry: 'resetErrors',
          invoke: { src: 'signUp' },
          on: {
            SIGNUP_SUCCESS: { target: 'authorized', actions: 'assignUser' },
            SIGNUP_FAILURE: { target: 'unauthorized', actions: 'assignErrors' },
          },
        },
        authorized: {
          on: {
            UPDATE_USER: 'updating',
            LOGOUT: { target: 'unauthorized', actions: 'logout' },
          },
        },
        updating: {},
      },
    },
  1. Define the states
  2. Define the events for the unauthorized state
  3. Define the effects
  4. Define the context
  5. Define the fire & forget effects

CREATE A STATE MACHINE WITH XSTATE

xstate & angular

  • It's quite easy to start using XState in Angular, however...
  • There are not enough examples/ a common pattern, helper functions to create a state machine
  • Maybe some of the reasons that it hasn't been adopted yet from the Angular community

xstate & angular

import { Machine, interpret } from 'xstate';
import { from } from 'rxjs';

const machine = Machine(/* ... */);
const service = interpret(machine).start();

const state$ = from(service);

state$.subscribe(state => {
  // ...
});

A stateless, enriched version of the FSM configuration 

A stateful, subscribable instance of the FSM 

An Observable from the Observable-like service

Observe the changes in the instance of the state machine

xstate & angular

  • We might have more requirements:
    • Start the state machine with a rehydrated state
    • Pass a configuration object to our state machine
    • Stop the state machine automatically after the component is destroyed 
    • Share the values of the Observable among several subscribers
import { Injectable } from '@angular/core';
import { OnDestroy } from '@angular/core';
import {
  EventObject,
  MachineOptions,
  interpret,
  State,
  InterpreterOptions,
  Interpreter,
  StateMachine,
  Typestate,
} from 'xstate';
import { filter, shareReplay, takeUntil } from 'rxjs/operators';
import { from, ReplaySubject } from 'rxjs';
import { InterpretedService, UseMachineOptions } from './types';

@Injectable()
export class XstateAngular<
  TContext,
  TStateSchema,
  TEvent extends EventObject = EventObject,
  TTypestate extends Typestate<TContext> = { value: any; context: TContext }
> implements OnDestroy {
  private service: Interpreter<TContext, TStateSchema, TEvent, TTypestate>;
  private readonly unsubscribeSubject$ = new ReplaySubject<void>(1);
  readonly unsubscribe$ = this.unsubscribeSubject$.asObservable();

  useMachine(
    machine: StateMachine<TContext, TStateSchema, TEvent, TTypestate>,
    options: Partial<InterpreterOptions> &
      Partial<UseMachineOptions<TContext, TEvent>> &
      Partial<MachineOptions<TContext, TEvent>> = {}
  ): InterpretedService<TContext, TStateSchema, TEvent, TTypestate> {
    const {
      context,
      guards,
      actions,
      activities,
      services,
      delays,
      state: rehydratedState,
      ...interpreterOptions
    } = options;

    const machineConfig = {
      context,
      guards,
      actions,
      activities,
      services,
      delays,
    };

    const createdMachine = machine.withConfig(machineConfig, {
      ...machine.context,
      ...context,
    } as TContext);

    this.service = interpret(createdMachine, interpreterOptions).start(
      rehydratedState ? (State.create(rehydratedState) as any) : undefined
    );

    const state$ = from(this.service).pipe(
      filter(
        ({ changed, event }) =>
          changed ||
          (changed === undefined && !!rehydratedState) ||
          (changed === undefined && !!(event?.type === 'xstate.init'))
      ),
      shareReplay(1),
      takeUntil(this.unsubscribe$)
    );

    return { state$, send: this.service.send, service: this.service };
  }

  ngOnDestroy() {
    this.service.stop();
    this.unsubscribeSubject$.next();
  }
}
rehydratedState = JSON.parse(localStorage.getItem('authState'));

authMachine = useMachine(this.authMachineConfig, {
  devTools: !environment.production,
  state: this.rehydratedState,
});

//authMachine: {state$, send, service}

xstate & angular

import { Machine } from 'xstate';
import { InterpretedService, XstateAngular } from 'xstate-angular';
// other imports
 
const toggleMachine = Machine({
  id: 'toggle',
  initial: 'stateOff',
  states: {
    stateOn: {
      on: { TOGGLE: 'stateOff' },
    },
    stateOff: {
      on: { TOGGLE: 'stateOn' },
    },
  },
});
 
@Component({
  selector: 'xstate-angular-toggle',
  template: `
    <h3>stateOn: {{ stateOn$ | async }}</h3>
    <h3>stateOff: {{ stateOff$ | async }}</h3>
    <button (click)="toggle()">toggle</button>
  `,
  providers: [XstateAngular],
})
export class ToggleComponent implements OnInit {
  stateOn$: Observable<boolean>;
  stateOff$: Observable<boolean>;
  service: InterpretedService<
    ToggleMachineContext,
    ToggleMachineSchema,
    ToggleMachineEvent
  >;
 
  constructor(
    private readonly xstateAngular: XstateAngular<
      ToggleMachineContext,
      ToggleMachineSchema,
      ToggleMachineEvent 
    >
  ) {
    this.service = this.xstateAngular.useMachine(toggleMachine);
  }
 
  ngOnInit() {
    this.stateOn$ = this.service.state$.pipe(map((s) => s.matches('stateOn')));
    this.stateOff$ = this.service.state$.pipe(
      map((s) => s.matches('stateOff'))
    );
  }
 
  toggle() {
    this.service.send({ type: 'TOGGLE' });
  }
}

xstate & angular

  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION

Agenda

  • TAKEAWAY
 
  • WHAT IS A STATE MACHINE, A STATECHART, XSTATE

Why I should start Using xstate?

But...you can ask yourself

  1. Is the business logic embedded/spread across your components/templates?
  • One source of truth for the business logic/behavior
  • Framework agnostic - the backbone of your app can be migrated immediately to another framework
  • The business logic/behavior is decoupled from the components - easier to be tested
  • Readability
  • Easier to extend the current functionality

With XState:

But...you can ask yourself

  • Everyone can understand the visualization of the state machines
  • Less back and forth communication between BA & devs
  • Can be a useful documentation

With XState:

2. Do I have a common language in my team?

3. Do I need documentation for the business logic?

But...you can ask yourself

  • It forces us to think about the solution to a problem upfront
  • It prevents edge cases - edge cases more visible

 

With XState:

4. Is my codebase unpredictable and buggy?

  • WHAT IS XSTATE, STATE MACHINES & STATECHARTS
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION
  • TAKEAWAY
 

I AKSED MYSELF

  • A use case in my company
  • Two routes with three panels which are open/closed/half-open based on different conditions
  • The business logic was spread in 5 different components
  • Now, we have a common source of truth, the configuration of the state machine

I AKSED MYSELF

  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION

Agenda

  • TAKEAWAY
 
  • WHAT IS A STATE MACHINE, A STATECHART, XSTATE

My dream

My dream

  • The BA is responsible to create the state machine, to express the intended behavior
  • The devs can use the same state machine configuration for the implementation
  • The QA team can start implementing the E2E tests with test scenarios provided automatically from the state machine config
  • A common source of truth for the requirements, documentation, testing, development
  • You have the tools to work like this

Thank you

xstate-angular

By Stefanos Lignos

xstate-angular

  • 1,062