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 { Machine, interpret } from 'xstate';
import { from } from 'rxjs';
const rehydratedState =
JSON.parse(localStorage.getItem('authState'));
const machine = Machine(authMachineConfig);
const service = interpret(machine).start(rehydratedState);
const state$ = from(service).pipe(
tap((state) => localStorage.setItem('auth', JSON.stringify(state))));
state$.subscribe(state => {
// ...
});
when refresh the page, start the machine from the authorized state
persist the current state of the state machine for every transition
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?.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 };
}
rehydratedState = JSON.parse(localStorage.getItem('authState'));
authMachine = useMachine(this.authMachineConfig, {
devTools: !environment.production,
state: this.rehydratedState,
persist: { key: 'authState' },
});
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