David Khourshid β @davidkpiano
stately.aiΒ
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.
let count = 0;
count++;
const count = 0;
const nextCount = count + 1;
(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;
}
π©βπ»
π§
π©βπ³
βοΈβ
π
πΆβ
πΆ
πβ
βοΈ
βοΈ
π©βπ»
π§
I would like a coffee...
What would you like?
Β ActorΒ
I would like a coffee, please.
Β ActorΒ
π
π¬
π¬
Here you go. βοΈ
Thanks!
someActor.send({
type: 'greet',
value: 'Buna ziua',
from: self
});
// ...
switch (event.type) {
case 'greet':
message.from.send({
type: 'greet',
value: '...',
from: self
});
// ...
}
{
orders: [/* ... */],
inventory: {
// ...
},
sales: 134.65
}
send(event)
Event-based
setState(value)
Value-based
// Read state
actor.subscribe((state) => {
console.log(state);
});
// Update state (indirectly)
actor.send({ type: 'inc' });
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
Event
const machine = {
initial: 'cart',
states: {
cart: {
on: {
CHECKOUT: 'shipping'
}
},
shipping: {
on: {
NEXT: 'contact'
}
},
contact: {
// ...
},
// ...
}
}
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
yarn add xstate@beta
createMachine({
initial: 'asleep',
states: {
asleep: {
on: {
espresso: 'awake'
}
},
awake: {/* ... */}
}
});
fromTransition((state, event) => {
// ...
return nextState;
}, initialState);
fromPromise(async () => {
const data = await getData();
return data;
});
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
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
1
2
3
4
UI framework
State management
JSHeroes 2023
David Khourshid β @davidkpiano
stately.ai