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 XSTATE, STATE MACHINES & STATECHARTS
  • 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

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

Angular Athens

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

Agenda

Disclaimer: This is not a detailed explanation of XState, but a presentation about something that you might find useful/inspiring to apply in your project!
  • TAKEAWAY
 

A library for creating finite state machines and statecharts

 

Xstate

 

What is

What is

 A FINITE STate machine

The life of a programmer as a finite state machine

  • David Harel - 1982
  • Created the language for statecharts
  • A common language for all the engineers in Israel Aircraft Industries
  • SCXML (State Chart XML)
  • Finite State Machines extended with the notions of orthogonality and hierarchy

What is

 A statechart

What is

 A FINITE STate machine

The life of a programmer as a finite state machine

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
 
  • WHAT IS XSTATE, STATE MACHINES & STATECHARTS
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION

Agenda

  • TAKEAWAY
 
authMachine(state, event) {
  if (state === 'unauthorized') {
    if (event.type === 'SIGNIN') {
      return 'signing_in';
    }
  } else if (state === 'authorized') {
    if (event.type === 'LOGOUT') {
      return 'unauthorized';
    }
  } else if (state === 'signing_in') {
    if (event.type === 'SIGNIN_SUCCESS') {
      return 'authorized';
    }
    if (event.type === 'SIGNIN_FAILURE') {
      return 'unauthorized';
    }
  }
}

transition(state, event) {
  this.state$.next({
    value: this.authMachine(state, event),
  });
}

state$ = new BehaviorSubject<State>({
  value: 'unauthorized',
});

How WE CAN CREATE A FINITE STATE MACHINE

A state machine for an authentication process

How WE CAN CREATE A STATE MACHINE

authMachine = {
  states: {
    unauthorized: {
      on: {
        SIGNIN: 'signin',
      },
    },
    authorized: {
      on: {
        LOGOUT: 'unauthorized',
      },
    },
    signing_in: {
      on: {
        SIGNIN_SUCCESS: 'authorized',
        SIGNIN_FAILURE: 'unauthorized',
      },
    },
  },
};

transition(state, event) {
  const nextState = this.authMachine[state].on[event];
  this.state$.next({
    value: nextState,
  });
}

state$ = new BehaviorSubject<State>({
  value: 'unauthorized',
});

A state machine for an authentication process

How WE CAN CREATE A STATE MACHINE

A state machine for an authentication process

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: {},
        signing_in: {},
        signing_up: {},
        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: 'signing_in',
            SIGNUP: 'signing_up',
          },
        },
        signing_in: {},
        signing_up: {},
        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: (_, event: SignIn) =>
          this.authService
            .login({ email: event.username, password: event.password })
            .pipe(
              map((user) => SignInSuccess(user)),
              catchError((result) => of(SignInFail(result.error.errors)))
            ),
        signUp: (_, 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 as SignInSuccess).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: (_, 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: (_, 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

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 

xstate & angular

(Example)

We defined our machine...the next step is to create a service and start listening to changes in the machine

rehydratedState = JSON.parse(localStorage.getItem('authState'));

authMachine = useMachine(this.ɵauthMachine, {
  devTools: !environment.production,
  state: this.rehydratedState,
  persist: { key: 'authState' },
});

xstate & angular

(Example)

import {
  EventObject,
  StateConfig,
  MachineOptions,
  interpret,
  State,
  InterpreterOptions,
  Interpreter,
  StateMachine,
  Typestate,
  StateSchema,
} from 'xstate';
import { filter, shareReplay, finalize, tap } from 'rxjs/operators';
import { Observable, from, BehaviorSubject, Subject } from 'rxjs';

export type InterpretedService<
  TContext,
  TStateSchema extends StateSchema = any,
  TEvent extends EventObject = EventObject,
  TTypestate extends Typestate<TContext> = any
> = {
  state$: Observable<State<TContext, TEvent>>;
  send: Interpreter<TContext, TStateSchema, TEvent, TTypestate>['send'];
  service: Interpreter<TContext, TStateSchema, TEvent, TTypestate>;
};

export interface UseMachineOptions<TContext, TEvent extends EventObject> {
  /**
   * If provided, will be merged with machine's `context`.
   */
  context?: Partial<TContext>;
  /**
   * The state to rehydrate the machine to. The machine will
   * start at this state instead of its `initialState`.
   */
  state?: StateConfig<TContext, TEvent>;
}

export function useMachine<
  TContext,
  TStateSchema extends StateSchema = any,
  TEvent extends EventObject = EventObject,
  TTypestate extends Typestate<TContext> = any
>(
  machine: StateMachine<TContext, TStateSchema, TEvent>,
  options: Partial<InterpreterOptions> &
    Partial<UseMachineOptions<TContext, TEvent>> &
    Partial<MachineOptions<TContext, TEvent>> = {}
): InterpretedService<TContext, TStateSchema, TEvent, TTypestate> {
  const {
    context,
    guards,
    actions,
    activities,
    services,
    delays,
    persist,
    state,
    ...interpreterOptions
  } = options;

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

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

  const service = interpret(createdMachine, interpreterOptions).start(
    state ? State.create(state) : undefined
  );

  if (!!persist && persist.key) {
    const state$ = from(service).pipe(
      tap((state) => localStorage.setItem(persist.key, JSON.stringify(state))),
      shareReplay(1)
    );
    return { state$, send: service.send, service };
  }

  const state$ = from(service).pipe(
    filter(({ changed }) => changed),
    shareReplay(1)
  );

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

export function useService<
  TContext,
  TStateSchema extends StateSchema = any,
  TEvent extends EventObject = EventObject,
  TTypestate extends Typestate<TContext> = any
>(
  service: Interpreter<TContext, TStateSchema, TEvent, TTypestate>
): InterpretedService<TContext, TStateSchema, TEvent, TTypestate> {
  const state$ = from(service).pipe(
    shareReplay(1),
    finalize(() => service.stop())
  );

  return { state$, send: service.send, service };
}
  • Interpret the machine we have created
  • Get an observable from the service

Testing

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

Agenda

  • TAKEAWAY
 

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
 
  • WHAT IS XSTATE, STATE MACHINES & STATECHARTS
  • HOW TO  CREATE/USE THEM WITH ANGULAR
 
  • WHEN TO USE THEM
 
  • WHY THIS PRESENTATION

Agenda

  • TAKEAWAY
 

My dream

Thank you

StateCharts !== REDUX

xstate-zivver

By Stefanos Lignos

xstate-zivver

  • 215