Agenda
It might be the software entropy
Out of the tar pit
Ben Moseley - Peter Marks
What is the complexity of a system?
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?
State
Control
Lack of understanding
What we try to do most of the time is to read the source code and understand it
State
Control
What is the complexity of a system?
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
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
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!
A library for creating finite state machines and statecharts
@DavidKPiano - Creator of XState
The life of a programmer as a finite state machine
Hierarchy
Orthogonality
The life of a programmer as a statechart
Guarded Transitions
History nodes
Delayed events & transitions
A second developer
who works in parallel
Agenda
A state machine for an authentication process
private authMachine = Machine<
AuthMachineContext,
AuthMachineSchema,
AuthMachineEvent
>(
{
id: 'auth',
initial: 'unauthorized',
states: {
unauthorized: {},
signin: {},
signup: {},
authorized: {},
updating: {},
},
},
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: {},
},
},
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)))
),
},
},
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: {},
},
},
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)))
),
},
},
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: {},
},
},
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
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}
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' });
}
}
Agenda
With XState:
With XState:
2. Do I have a common language in my team?
3. Do I need documentation for the business logic?
With XState:
4. Is my codebase unpredictable and buggy?
Agenda