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: {},
},
},
- 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: {},
},
},
Define the states- 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)))
),
},
},
Define the statesDefine the events for the unauthorized state- 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: {},
},
},
Define the statesDefine the events for the unauthorized stateDefine the effects- 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)))
),
},
},
Define the statesDefine the events for the unauthorized stateDefine the invoked effectsDefine the context- 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: {},
},
},
Define the statesDefine the events for the unauthorized stateDefine the effectsDefine the context- 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
- 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
- 320