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 A STATE MACHINE, A STATECHART, XSTATE
- 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
Ben Moseley - Peter Marks
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
Not always dressed like this
Angular Athens
- WHAT IS XSTATE, STATE MACHINES & STATECHARTS
- HOW TO CREATE/USE THEM WITH ANGULAR
- WHEN TO USE THEM
WHY THIS PRESENTATION
- TAKEAWAY
- HOW TO CREATE/USE THEM WITH ANGULAR
- WHEN TO USE THEM
WHY THIS PRESENTATION
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!
- TAKEAWAY
- WHAT IS A STATE MACHINE, A STATECHART, XSTATE
A library for creating finite state machines and statecharts
Xstate
What is
@DavidKPiano - Creator of XState
What is
A FINITE STate machine
The life of a programmer as a finite state machine
- David Harel - 1982
- A common language for all the engineers in Israel Aircraft Industries
- Finite State Machines extended with the notions of orthogonality and hierarchy
What is
A statechart
What is
A statechart
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
A second developer
who works in parallel
- HOW TO CREATE/USE THEM WITH ANGULAR
- WHEN TO USE THEM
WHY THIS PRESENTATION
Agenda
- TAKEAWAY
WHAT IS A STATE MACHINE, A STATECHART, XSTATE
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: {},
signin: {},
signup: {},
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: 'signin',
SIGNUP: 'signup',
},
},
signin: {},
signup: {},
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: (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)))
),
},
},
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.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)))
),
},
},
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
- It's quite easy to start using XState in Angular, however...
- There are not enough examples/ a common pattern, helper functions to create a state machine
- Maybe some of the reasons that it hasn't been adopted yet from the Angular community
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
An Observable from the Observable-like service
Observe the changes in the instance of the state machine
xstate & angular
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 };
}
xstate & angular
rehydratedState = JSON.parse(localStorage.getItem('authState'));
authMachine = useMachine(this.authMachineConfig, {
devTools: !environment.production,
state: this.rehydratedState,
persist: { key: 'authState' },
});
xstate & angular
HOW TO CREATE/USE THEM WITH ANGULAR
- WHEN TO USE THEM
WHY THIS PRESENTATION
Agenda
- TAKEAWAY
WHAT IS A STATE MACHINE, A STATECHART, XSTATE
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
HOW TO CREATE/USE THEM WITH ANGULAR
WHEN TO USE THEM
WHY THIS PRESENTATION
Agenda
- TAKEAWAY
WHAT IS A STATE MACHINE, A STATECHART, XSTATE
My dream
My dream
- The BA is responsible to create the state machine, to express the intended behavior
- The devs can use the same state machine configuration for the implementation
- The QA team can start implementing the E2E tests with test scenarios provided automatically from the state machine config
- A common source of truth for the requirements, documentation, testing, development
- You have the tools to work like this
Thank you ♡
xstate-angular-nl
By Stefanos Lignos
xstate-angular-nl
- 2,470