Effective state machines
for complex logic
- Make API call to auth provider
- Initiate OAuth flow
- Ensure token is valid
- Persist token as cookie
- Redirect to logged in view
Given a user is logged out,
When the user logs in with correct credentials
Then the user should be logged in
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482116/CleanShot_2023-05-18_at_09.12.55_2x.png)
function transition(state, event) {
switch (state.value) {
case 'cart':
if (event.type === 'CHECKOUT') {
return { value: 'shipping' };
}
return state;
case 'shipping':
// ...
default:
return state;
}
}
State machines
with switch statements
State
Event
const machine = {
initial: 'cart',
states: {
cart: {
on: {
CHECKOUT: 'shipping'
}
},
shipping: {
on: {
NEXT: 'contact'
}
},
contact: {
// ...
},
// ...
}
}
State machines
with object lookup
State
Event
State machines
with object lookup
function transition(state, event) {
const nextState = machine
.states[state]
.on?.[event.type]
?? state;
}
transition('cart', { type: 'CHECKOUT' });
// => 'shipping'
transition('cart', { type: 'UNKNOWN' });
// => 'cart'
const machine = {
initial: 'cart',
states: {
cart: {
on: {
CHECKOUT: 'shipping'
}
},
shipping: {
on: {
NEXT: 'contact'
}
},
contact: {
// ...
},
// ...
}
}
import { createMachine } from 'xstate';
const machine = createMachine({
initial: 'cart',
states: {
cart: {
on: {
CHECKOUT: 'shipping'
}
},
shipping: {
on: {
NEXT: 'contact'
}
},
contact: {
// ...
},
// ...
}
});
import { setup } from 'xstate';
const machine = setup({
types: {/* ... */},
actions: {
// Action implementations
},
actors: {
// Actor implementations
},
guards: {
// Guard implementations
},
}).createMachine({
initial: 'cart',
states: {
cart: {
on: {
CHECKOUT: 'shipping'
}
},
shipping: {
on: {
NEXT: 'contact'
}
},
contact: {
// ...
},
// ...
}
});
import { setup, createActor } from 'xstate';
const machine = setup({
// ...
}).createMachine({
// ...
});
const actor = createActor(machine);
actor.subscribe(snapshot => {
console.log(snapshot.value);
});
actor.start();
// logs 'cart'
actor.send({ type: 'CHECKOUT' });
// logs 'shipping'
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11139859/CleanShot_2024-02-21_at_16.12.18_2x.png)
state.new
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11139859/CleanShot_2024-02-21_at_16.12.18_2x.png)
The actor model
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
The actor model
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
The actor model
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
The actor model
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
The actor model
๐งโ๐ป
๐ง
๐ฉโ๐ณ
โ๏ธโ
๐
๐ถโ
๐ถ
๐โ
โ๏ธ
โ๏ธ
๐งโ๐ป
๐ง
I would like a coffee...
What would you like?
ย Actorย
Einen Kaffee bitte
ย Actorย
๐ญ
๐ฌ
๐ฌ
Here you go, American โ๏ธ
Dankeschรถn!
{
orders: [/* ... */],
inventory: {
// ...
},
sales: 134.65
}
Actor logic
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor System
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
processing...
state ??
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor mailboxes
โ๏ธ
โ๏ธ
โ๏ธ
โ๏ธ
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
โ๏ธ
โ๏ธ
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
state 3
โ๏ธ
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
state 3
โ๏ธ
state 4
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11090366/CleanShot_2024-01-31_at_09.01.51_2x.png)
Actor mailboxes
import {
fromPromise
} from 'xstate';
const promiseLogic = fromPromise(async ({ input }: {
input: { userId: string }
}) => {
// TODO: use Effect
const user = await getUser(input.userId);
return user;
});
Promise logic
import {
fromTransition
} from 'xstate';
const countLogic = fromTransition((state, event) => {
switch (event.type) {
case 'increment': {
return { count: state.count + 1 };
}
case 'decrement': {
return { count: state.count - 1 };
}
default: {
return state;
}
}
}, { count: 0 }); // Initial state
Transition logic
import {
fromCallback
} from 'xstate';
const resizeLogic = fromCallback(({ sendBack, receive }) => {
const resizeHandler = (event) => {
sendBack(event);
};
window.addEventListener('resize', resizeHandler);
const removeListener = () => {
window.removeEventListener('resize', resizeHandler);
}
receive(event => {
if (event.type === 'stopListening') {
console.log('Stopping listening');
removeListener();
}
});
// Cleanup function
return () => {
console.log('Cleaning up');
removeListener();
}
});
Callback logic
import {
fromObservable
} from 'xstate';
import { interval } from 'rxjs';
const intervalLogic = fromObservable(
({ input }: { input: number }) => interval(input));
Observable logic
import {
setup
} from 'xstate';
const machineLogic = setup({
// ...
}).createMachine({
initial: 'speaking',
states: {
speaking: {
// ...
},
relaxing: {
type: 'final'
}
}
});
State machine logic
import {
createActor
} from 'xstate';
import { someLogic } from './someLogic';
const actor = createActor(someLogic, {
input: {/* some input */}
});
actor.subscribe(snapshot => {
console.log(snapshot.status, snapshot.output);
});
actor.start();
actor.send({ type: 'someEvent', data: 42 })
Actors
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11143804/CleanShot_2024-02-23_at_08.02.49_2x.png)
zio.dev/zio-actors/
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11143808/zio__1_.png)
![](https://media1.giphy.com/media/z21at1V8CRoWs/giphy.gif)
@statelyai/inspect
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11144075/CleanShot_2024-02-23_at_10.36.27_2x.png)
import {
createBrowserInspector
} from '@statelyai/inspect';
const inspector = createBrowserInspector();
inspector.actor('speaker');
inspector.actor('listener');
inspector.event('speaker', 'question?', {
source: 'listener'
});
inspector.event('listener', 'answer!', {
source: 'speaker'
});
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/11139897/CleanShot_2024-02-21_at_16.33.47_2x.png)
export const machine = setup({
types: {
context: {} as {
videoRef: VideoRef;
videoUrl?: string;
},
input: {} as {
videoRef: VideoRef;
},
},
actions: {
playVideo: (_, params: { videoRef: VideoRef }) =>
Effect.sync(() => {
params.videoRef.current?.play();
}).pipe(Effect.runSync),
pauseVideo: (_, params: { videoRef: VideoRef }) =>
Effect.sync(() => {
params.videoRef.current?.pause();
}).pipe(Effect.runSync),
},
actors: {
loadVideo: fromPromise(() => Effect.runPromise(program)),
videoEnded: fromCallback<any, { videoRef: any }>(({ input, sendBack }) => {
const videoRef = input.videoRef;
const onEnded = () => {
Effect.sync(() => {
sendBack({ type: 'video.ended' });
}).pipe(Effect.runSync);
};
videoRef.current?.addEventListener('ended', onEnded);
return () => {
videoRef.current?.removeEventListener('ended', onEnded);
};
}),
keyEscape: fromCallback(({ input, sendBack }) => {
const onKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
Effect.sync(() => {
sendBack({ type: 'key.escape' });
}).pipe(Effect.runSync);
}
};
window.addEventListener('keydown', onKeydown);
return () => {
window.removeEventListener('keydown', onKeydown);
};
}),
}
}).createMachine({
// ...
})
const machine = setup({
// ...
}).createMachine({
id: 'Video player machine',
context: ({ input }) => ({
videoRef: input.videoRef
}),
initial: 'loading',
states: {
loading: {
invoke: {
src: 'loadVideo',
onDone: {
target: 'mini',
},
},
},
mini: {
id: 'mini',
on: {
toggle: {
target: 'full',
},
},
},
full: {
entry: {
type: 'playVideo',
params: ({ context }) => ({
videoRef: context.videoRef,
}),
},
exit: {
type: 'pauseVideo',
params: ({ context }) => ({
videoRef: context.videoRef,
}),
},
invoke: [
{
src: 'videoEnded',
input: ({ context }) => ({ videoRef: context.videoRef }),
},
{
src: 'keyEscape',
},
],
initial: 'playing',
states: {
playing: {
on: {
'video.ended': {
target: 'stopped',
},
},
},
stopped: {
after: {
'2000': {
target: '#mini',
},
},
},
},
on: {
toggle: {
target: 'mini',
},
'key.escape': {
target: 'mini',
},
},
},
},
});
sandromaglione.com/articles/getting-started-with-xstate-and-effect-audio-player
@SandroMaglione
State machine
State
Effect
Effect
State
Effect
Effect
Effect
![](https://media4.giphy.com/media/xsF1FSDbjguis/giphy.gif)
Stateful thinking
Effectful thinking
Thank you!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/9674899/logomark-black-nobg.png)
Resources
Effect Days 2024
David Khourshid ยท @davidkpiano
stately.ai
Effective state machines for complex logic
By David Khourshid
Effective state machines for complex logic
- 150