Agenda
It might be the software entropy
Out of the tar pit
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
Angular Athens
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!
A library for creating finite state machines and statecharts
The life of a programmer as a finite state machine
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
Agenda
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',
});
A state machine for an authentication process
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
A state machine for an authentication process
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: {},
},
},
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: {},
},
},
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)))
),
},
},
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 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)))
),
},
},
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
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' },
});
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 };
}
Testing
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