The two ways
David Khourshid β @davidkpiano
stately.aiΒ
to manage state
JSHeroes 2023
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;
});
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.
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;
}
- Recoil
- Valtio
- MobX
- Jotai
- XState
Multi-store
- Redux
- Zustand
- Vuex
- Pinia
Single-store
Single-store is convenient,
multi-store is realistic.
π©βπ»
π§
π©βπ³
βοΈβ
π
πΆβ
πΆ
πβ
βοΈ
βοΈ
π©βπ»
π§
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: 'Buna ziua',
from: self
});
// ...
switch (event.type) {
case 'greet':
message.from.send({
type: 'greet',
value: '...',
from: self
});
// ...
}
All computation is performed
within an actor
Actors can communicate
only through messages
In response to a message,
an actor can:
Change its state/behavior
In response to a message,
an actor can:
Change its state/behavior
Send messages to
other actors
In response to a message,
an actor can:
Change its state/behavior
Send messages to
other actors
Spawn new actors
Actor logic
{
orders: [/* ... */],
inventory: {
// ...
},
sales: 134.65
}
Finite state (behavior)
Extended state (contextual data)
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
Everything is a
graph
- 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
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'
Demo
(please work conference wifi π)
Play this if the demo fails
v5 beta
yarn add xstate@beta
v5 beta
createMachine({
initial: 'asleep',
states: {
asleep: {
on: {
espresso: 'awake'
}
},
awake: {/* ... */}
}
});
State machines
fromTransition((state, event) => {
// ...
return nextState;
}, initialState);
Transition functions
fromPromise(async () => {
const data = await getData();
return data;
});
Promises
import { createMachine, interpret, assign } from 'xstate';
const counterMachine = createMachine({
context: { count: 0 },
on: {
inc: {
actions: assign({ count: ({ context }) => context.count + 1 })
},
dec: {
actions: assign({ count: ({ context }) => context.count - 1 })
}
}
});
const counterActor = interpret(counterMachine).start();
counterActor.send({ type: 'inc' });
counterActor.send({ type: 'inc' });
counterActor.send({ type: 'dec' });
counterActor.getSnapshot().context.count;
// => 1
-
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
UI = fn(state)
[state, effects] =
fn(prevState, event)
UI framework
State management
Independence can be achieved
with any state management solution
Two ways to manage state:
Easy vs. simple
Which will you choose?
MulΘumesc JSHeroes!
Resources
JSHeroes 2023
David Khourshid β @davidkpiano
stately.ai
Two types of state management
By David Khourshid
Two types of state management
- 967