David Khourshid · @davidkpiano
stately.ai
🧮
function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<output>{count}</output>
<button onClick={() => setCount(count + 1)}>Add</button>
<button onClick={() => setCount(count - 1)}>Subtract</button>
</section>
);
}
function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<output>{count}</output>
<button
onClick={() => {
if (count < 10) setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
if (count > 0) setCount(count - 1);
}}
>
Subtract
</button>
</section>
);
}
function Counter() {
const [count, setCount] = useState(0);
function increment(count) {
if (count < 10) setCount(count + 1);
}
function decrement(count) {
if (count > 0) setCount(count - 1);
}
return (
<section>
<output>{count}</output>
<input
type="number"
onBlur={(e) => {
setCount(e.target.valueAsNumber);
}}
/>
<button onClick={increment}>Add</button>
<button onClick={decrement}>Subtract</button>
</section>
);
}
function Counter() {
const [count, setCount] = useState(0);
function changeCount(val) {
if (val >= 0 && val <= 10) {
setCount(val);
}
}
return (
<section>
<output>{count}</output>
<input
type="number"
onBlur={(e) => {
changeCount(e.target.valueAsNumber);
}}
/>
<button
onClick={(e) => {
changeCount(count + 1);
}}
>
Add
</button>
<button
onClick={(e) => {
changeCount(count - 1);
}}
>
Subtract
</button>
</section>
);
}
function Counter() {
const [count, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
return (
<section>
<output>{count}</output>
<input
type="number"
onBlur={(e) => {
send({ type: "set", value: e.target.valueAsNumber });
}}
/>
<button
onClick={() => {
send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
const CountContext = createContext();
function CountView() {
const count = useContext(CountContext);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
// send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
// send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
function App() {
const [count, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
return (
<CountContext.Provider value={count}>
<CountView />
</CountContext.Provider>
);
}
const CountContext = createContext();
function CountView() {
const [count, send] = useContext(CountContext);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
export function App() {
const [count, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
return (
<CountContext.Provider value={[count, send]}>
<CountView />
</CountContext.Provider>
);
}
const CountContext = createContext();
function CountView() {
const countStore = useContext(CountContext);
const [count, setCount] = useState(0);
useEffect(() => {
return countStore.subscribe(setCount);
}, []);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
countStore.send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
countStore.send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
function useCount() {
const [state, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
const listeners = useRef(new Set());
useEffect(() => {
listeners.current.forEach((listener) => listener(state));
}, [state]);
return {
send,
subscribe: (listener) => {
listeners.current.add(listener);
return () => {
listeners.current.delete(listener);
};
}
};
}
export function App() {
const countStore = useCount();
return (
<CountContext.Provider value={countStore}>
<CountView />
</CountContext.Provider>
);
}
const CountContext = createContext();
function useSelector(store, selectFn) {
const [state, setState] = useState(store.getSnapshot());
useEffect(() => {
return store.subscribe((newState) => setState(selectFn(newState)));
}, []);
return state;
}
function CountView() {
const countStore = useContext(CountContext);
const count = useSelector(countStore, (count) => count);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
countStore.send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
countStore.send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
function useCount() {
const [state, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
const listeners = useRef(new Set());
useEffect(() => {
listeners.current.forEach((listener) => listener(state));
}, [state]);
return {
send,
subscribe: (listener) => {
listeners.current.add(listener);
return () => {
listeners.current.delete(listener);
};
},
getSnapshot: () => state
};
}
function App() {
const countStore = useCount();
return (
<CountContext.Provider value={countStore}>
<CountView />
</CountContext.Provider>
);
}
setState(value)
dispatch(event)
import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';
const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
const charCountState = selector({
key: 'charCountState', // unique ID (with respect to other atoms/selectors)
get: ({get}) => {
const text = get(textState);
return text.length;
},
});
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
function CharacterCount() {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
}
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' });
// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"
class Timer {
secondsPassed = 0
constructor() {
makeAutoObservable(this)
}
increaseTimer() {
this.secondsPassed += 1
}
}
const myTimer = new Timer()
// A function component wrapped with `observer` will react
// to any future change in an observable it used before.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
setInterval(() => {
myTimer.increaseTimer()
}, 1000)
import { atom, useAtom } from 'jotai'
// Create your atoms and derivatives
const textAtom = atom('hello')
const uppercaseAtom = atom(
(get) => get(textAtom).toUpperCase()
)
// Use them anywhere in your app
const Input = () => {
const [text, setText] = useAtom(textAtom)
const handleChange = (e) => setText(e.target.value)
return (
<input value={text} onChange={handleChange} />
)
}
const Uppercase = () => {
const [uppercase] = useAtom(uppercaseAtom)
return (
<div>Uppercase: {uppercase}</div>
)
}
// Now you have the components
const App = () => {
return (
<>
<Input />
<Uppercase />
</>
)
}
import { createSlice } from '@reduxjs/toolkit'
import { useSelector, useDispatch } from 'react-redux'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
const { increment, decrement, incrementByAmount } = counterSlice.actions
const incrementAsync = (amount) => (dispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
export function Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
import create from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button
onClick={increasePopulation}>
one up
</button>
}
import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';
const countMachine = createMachine({
context: {
count: 0
},
on: {
inc: {
actions: assign({
count: (ctx) => ctx.count + 1
})
},
dec: {
actions: assign({
count: (ctx) => ctx.count - 1
})
}
}
});
function App() {
const [state, send] = useMachine(countMachine);
return <main>
<button onClick={() => send({ type: 'inc' })}>
Increment
</button>
<button onClick={() => send({ type: 'dec' })}>
Decrement
</button>
</main>
}
(state, event) => (nextState, )
effects
(state, event) => nextState
const data = useSelector(/* ... */)
const Component = observer(() => {
return <div>{state.some.data}</div>
});
Selectors (manual)
Observers (automatic)
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
Logged out
Logged in
LOG IN
[correct credentials]
Given a user is logged out,
When the user logs in with correct credentials
Then the user should be logged in
interpret(machine)
interpret(fromPromise(...))
interpret(fromObservable(...))
interpret(fromReducer(...))
interpret(fromCallback(...))
Actor system architecture
Sequence diagrams
Callbacks
Event handlers
Ad-hoc logic
Ad-hoc logic
Not simple
Reducer
EVENT
Easy enough
EVENT
EVENT
EVENT
State machine
Not easy
EVENT
EVENT
EVENT
Statechart
Definitely not easy
App logic
$100
$100
$100
$100
🛒 Cart
3
Data flow tree !== UI tree
User interface
Send events
Read state
Store
1
2
3
4
With any state management solution
Make it work, make it right, make it fast