David Khourshid → @davidkpiano
stately.ai
State management
with
Managing state
is difficult.
function Conference() {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [location, setLocation] = useState('');
const [url, setUrl] = useState('');
const [image, setImage] = useState('');
const [price, setPrice] = useState(0);
const [attendees, setAttendees] = useState(0);
const [organizer, setOrganizer] = useState('');
const [countries, setCountries] = useState([]);
const [categories, setCategories] = useState([]);
const [tags, setTags] = useState([]);
const [swag, setSwag] = useState([]);
const [speakers, setSpeakers] = useState([]);
const [sponsors, setSponsors] = useState([]);
const [videos, setVideos] = useState([]);
const [tickets, setTickets] = useState([]);
const [schedule, setSchedule] = useState([]);
const [socials, setSocials] = useState([]);
const [coffee, setCoffee] = useState([]);
const [codeOfConduct, setCodeOfConduct] = useState('');
// ...
}
const [count, setCount] = useState(0);
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
let count = 0;
incButton.addEventListener('click', () => {
count++;
counter.textContent = count.toString();
}
decButton.addEventListener('click', () => {
count--;
counter.textContent = count.toString();
}
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
let count = 0;
incButton.addEventListener('click', () => {
if (count < 10) {
count++;
counter.textContent = count.toString();
}
});
decButton.addEventListener('click', () => {
if (count > 0) {
count--;
counter.textContent = count.toString();
}
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
let count = 0;
incButton.addEventListener('click', () => {
if (count < 10) {
count++;
counter.textContent = count.toString();
}
});
decButton.addEventListener('click', () => {
if (count > 0) {
count--;
counter.textContent = count.toString();
}
});
counter.addEventListener('keyup', (event) => {
if (event.key === 'ArrowUp' && count < 10) {
count++;
counter.textContent = count.toString();
} else if (event.key === 'ArrowDown' && count > 0) {
count--;
counter.textContent = count.toString();
}
});
counter.addEventListener('keydown', (event) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();
}
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
let count = 0;
function incrementCount() {
if (count < 10) {
count++;
counter.textContent = count.toString();
}
}
function decrementCount() {
if (count > 0) {
count--;
counter.textContent = count.toString();
}
}
incButton.addEventListener('click', incrementCount);
decButton.addEventListener('click', decrementCount);
counter.addEventListener('keyup', (event) => {
if (event.key === 'ArrowUp') {
incrementCount();
} else if (event.key === 'ArrowDown') {
decrementCount();
}
});
counter.addEventListener('keydown', (event) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();
}
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
const output = document.querySelector('#output');
function createObservable(initialValue) {
let value = initialValue;
const listeners = new Set();
return {
get() {
return value;
},
set(newValue) {
value = newValue;
listeners.forEach(listener => listener(value));
},
subscribe(listener) {
listeners.add(listener);
return { unsubscribe: () => listeners.delete(listener) }
},
};
}
const countObservable = createObservable(0);
incButton.addEventListener('click', () => {
if (count < 10) {
countObservable.set(count + 1);
}
});
decButton.addEventListener('click', () => {
if (count > 0) {
countObservable.set(count - 1);
}
});
counter.addEventListener('keyup', (event) => {
if (event.key === 'ArrowUp') {
countObservable.set(count + 1);
} else if (event.key === 'ArrowDown') {
countObservable.set(count - 1);
}
});
counter.addEventListener('keydown', (event) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();
}
});
countObservable.subscribe(count => {
counter.textContent = count;
output.textContent = count;
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
const output = document.querySelector('#output');
function createObservable(transitionFn, initialValue) {
let value = initialValue;
const listeners = new Set();
return {
get() {
return value;
},
send(event) {
value = transitionFn(value, event);
listeners.forEach((listener) => listener(value));
},
subscribe(listener) {
listeners.add(listener);
return { unsubscribe: () => listeners.delete(listener) }
},
};
}
const countObservable = createObservable((count, event) => {
switch (event.type) {
case 'inc':
return Math.min(10, count + 1);
case 'dec':
return Math.max(0, count - 1);
default:
return count;
}
}, 0);
incButton.addEventListener('click', () => {
countObservable.send({ type: 'inc' });
});
decButton.addEventListener('click', () => {
countObservable.send({ type: 'dec' });
});
counter.addEventListener('keyup', (event) => {
if (event.key === 'ArrowUp') {
countObservable.send({ type: 'inc' });
} else if (event.key === 'ArrowDown') {
countObservable.send({ type: 'dec' });
}
});
counter.addEventListener('keydown', (event) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();
}
});
countObservable.subscribe((count) => {
counter.textContent = count;
output.textContent = count;
});
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10437194/logo-title-light.png)
You just
reinvented
🎉
I don't need a
state management library.
I don't need a
state management library.
I should have used a
state management library.
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/9836835/1_fKV3_Y4usDYZKPsNp1yCvA.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/9836840/mobx.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/9836848/jotai-header-dark.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/8882077/redux-docs-logo-at-master-ruanyf-redux-docs-github-redux-png-3610_1490.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/4065778/rxjs.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482674/Pinialogo.svg.png)
Mutable state is fast,
immutable state is maintainable.
let count = 0;
count++;
const count = 0;
const nextCount = count + 1;
Effect management
is state management.
(state, event) => (nextState, )
effects
(state, event) => nextState
switch (state) {
case 'mini':
if (event.type === 'toggle') {
playVideo();
return 'full';
}
break;
case 'full':
if (event.type === 'toggle') {
pauseVideo();
return 'mini';
}
break;
default:
break;
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482344/CleanShot_2023-05-18_at_11.46.15_2x.png)
- Recoil
- Valtio
- MobX
- Jotai
- XState
Multi-store
- Redux
- Zustand
- Vuex
- Pinia
Single-store
Single-store is convenient,
multi-store is realistic.
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482650/pasted-from-clipboard.png)
👩💻
🧔
👩🍳
☕️❔
📝
💶❓
💶
📄✅
☕️
☕️
👩💻
🧔
I would like a coffee...
What would you like?
Actor
I would like a coffee, please.
Actor
💭
💬
💬
Here you go. ☕️
Thanks!
The actor model
someActor.send({
type: 'greet',
value: 'Buenos',
from: self
});
// ...
switch (event.type) {
case 'greet':
message.from.send({
type: 'greet',
value: 'Que tal',
from: self
});
// ...
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482240/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482127/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482127/pasted-from-clipboard.png)
All computation is performed
within an actor
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482127/pasted-from-clipboard.png)
Actors can communicate
only through messages
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482127/pasted-from-clipboard.png)
In response to a message,
an actor can:
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482137/pasted-from-clipboard.png)
Change its state/behavior
In response to a message,
an actor can:
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482137/pasted-from-clipboard.png)
Change its state/behavior
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482148/pasted-from-clipboard.png)
Send messages to
other actors
In response to a message,
an actor can:
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482137/pasted-from-clipboard.png)
Change its state/behavior
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482148/pasted-from-clipboard.png)
Send messages to
other actors
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482153/pasted-from-clipboard.png)
Spawn new actors
Actor logic
{
orders: [/* ... */],
inventory: {
// ...
},
sales: 134.65
}
Finite state (behavior)
Extended state (contextual data)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482100/CleanShot_2023-05-18_at_08.45.24_2x.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482113/pasted-from-clipboard.png)
The actor model is a great way
to model application logic.
Direct state management is easy,
indirect state management is simple.
Indirect
send(event)
Event-based
Direct
setState(value)
Value-based
// Read state
actor.subscribe((state) => {
console.log(state);
});
// Update state (indirectly)
actor.send({ type: 'inc' });
Everything is a
graph
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10437223/REST-vs-GraphQL-Seinfeld-Graph-2048x933.png.webp)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10437225/User-Registration-Flowchart.webp)
Everything is a
graph
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/9978112/e2b3ba7b8f347b91bfea4d28d8dbeec15edcffc8.jpeg)
- 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 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'
import { createMachine } from "xstate";
export const machine = createMachine({
initial: 'cart',
states: {
cart: {
on: {
CHECKOUT: { target: 'shipping' }
}
},
shipping: {
on: {
NEXT: { target: 'contact' }
}
},
contact: {
// ...
},
// ...
}
})
import { createActor } from 'xstate';
import { machine } from './ghostMachine';
const actor = createActor(machine);
actor.subscribe(state => {
console.log(state);
});
actor.start();
// { value: 'checkout', ... }
actor.send({ type: 'CHECKOUT' });
// { value: 'shipping', ... }
cart
shipping
contact
payment
confirmation
CHECKOUT
NEXT
NEXT
ORDER
PAYPAL
BACK
BACK
CANCEL
Identify
logical flaws
cart
shipping
contact
payment
confirmation
CHECKOUT
NEXT
NEXT
ORDER
PAYPAL
As a user, when I'm in the cart and I click the checkout button, I should be on the shipping page.
cart
shipping
contact
payment
confirmation
CHECKOUT
NEXT
NEXT
ORDER
PAYPAL
As a user, when I'm in the cart and I checkout via PayPal, I should be taken directly to the payment screen.
cart
shipping
contact
payment
confirmation
CHECKOUT
NEXT
NEXT
ORDER
PAYPAL
Shortest path
confirmation state
cart
shipping
contact
payment
confirmation
CHECKOUT
NEXT
NEXT
ORDER
PAYPAL
Shortest path
confirmation state where
shipping address is provided
-
Mutable vs. immutable
-
Effect management
-
Single vs. multi store
-
Direct vs. indirect
None of this matters (directly)
Correctness
Velocity
Maintenance
Bug-free
Intuitive UX
Accessible
No race conditions
Adheres to specifications
Verifiable logic
Adding features
Changing features
Removing features
Fixing bugs
Adjusting tests
Onboarding
Documentation
Understanding bug root causes
Performance
Testability
Stability at complexity scale
Which state management is
best for app logic?
The one that your entire team
can understand
Map requirements to code
1
Code independently of frameworks
2
Make interfaces simple (read, send)
3
Use a common visual language
4
Effective state management
Independence can be achieved
with any state management solution
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/10482358/pasted-from-clipboard.png)
Thank you!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/174419/images/9674899/logomark-black-nobg.png)
Resources
David Khourshid → @davidkpiano
stately.ai
Stately and XState
By David Khourshid
Stately and XState
- 235