Farzad Yousefzadeh
Lead developer @epicgames. Core @statelyai. Coach @mentorcruise. ❤️ Statecharts, Finite State Machines, and XState.
Scene #1
renderer (state) = view
Component library / Framework
State management tool
Tool specific approaches
Local state
Global state
store.dispatch({
type: "UPDATE_TITLE",
payload: "Holyjs"
})
@action onClick() {
this.props.title = "HolyJS"
}
const [title, setTitle] = useState("")
setTitle("HolyJS")
this.state = {
title: "";
}
this.setState({title: "HolyJS"})
new Vuex.Store({
state: {
title: ""
},
mutations: {
updateTitle: (state, payload) {
state.title = payload.title
}
}
})
source: http://tiny.cc/eg2hfz
GUI is event-driven
Alan Kay, the Dynabook, 1972
GUI is built upon events and messages
Listen to events and execute actions (side effects)
Scene #2
Event handler's logic based on previously happened events
Press the (-) button
Context aware event handlers
Constructing the User Interface with Statecharts
Ian Horrocks 1999
button.addEventListener("click", ...)
this.setState({ ... })
new Promise(...)
window.fetch(...)
if (typeof index === "boolean") {
addBefore = index;
index = null;
} else if (index < 0 || index >= _.slideCount) {
return false;
}
_.unload();
if (typeof index === "number") {
if (index === 0 && _.$slides.length === 0) {
$(markup).appendTo(_.$slideTrack);
} else if (addBefore) {
$(markup).insertBefore(_.$slides.eq(index));
} else {
$(markup).insertAfter(_.$slides.eq(index));
}
} else {
if (addBefore === true) {
$(markup).prependTo(_.$slideTrack);
} else {
$(markup).appendTo(_.$slideTrack);
}
}
{
value: "",
valid: false
}
{
value: "fars",
valid: false
}
{
value: "",
valid: false
}
{
value: "",
valid: false
}
{
value: "",
valid: false
}
{
value: "",
valid: false,
isValidated: false
}
{
value: "fars",
valid: false,
isValidated: true
}
{
value: "",
valid: false,
isValidated: true
}
{
value: "farskid@gmail.com",
valid: true,
isValidated: true
}
value.length | = 0 | > 0 |
valid | true | false |
isValidated | true | false |
value.length = 0, valid: false, isValidated: false
value.length = 0, valid: false, isValidated: true
value.length > 0, valid: false, isValidated: true
value.length > 0, valid: true, isValidated: true
value.length | = 0 | > 0 |
valid | true | false |
isValidated | true | false |
value.length = 0, valid: true, isValidated: false
value.length = 0, valid: true, isValidated: true
value.length > 0, valid: false, isValidated: false
value.length > 0, valid: true, isValidated: false
valid states:
impossible states:
v.length
valid
isValidated
function handleChange(newValue) {
if (!isValidated) {
setValidated(true);
}
if (newValue.length === 0 || !validateEmail(newValue)) {
setValid(false);
} else {
setValid(true);
}
setValue(newValue);
}
HIGHER CYCLOMATIC COMPLEXITY
LESS PREDICTABLE LOGIC
if, else,
while, for,
switch, case
Harder to track logic
if (typeof index === "boolean") {
addBefore = index;
index = null;
} else if (index < 0 || index >= _.slideCount) {
return false;
}
_.unload();
if (typeof index === "number") {
if (index === 0 && _.$slides.length === 0) {
$(markup).appendTo(_.$slideTrack);
} else if (addBefore) {
$(markup).insertBefore(_.$slides.eq(index));
} else {
$(markup).insertAfter(_.$slides.eq(index));
}
} else {
if (addBefore === true) {
$(markup).prependTo(_.$slideTrack);
} else {
$(markup).appendTo(_.$slideTrack);
}
}
{
inputs: {
email: {
value: "",
valid: false,
isValidated: false
}
},
submitting: false,
submitError: undefined
}
{
inputs: {
email: {
value: "fars",
valid: false,
isValidated: true
}
},
submitting: false,
submitError: undefined
}
{
inputs: {
email: {
value: "",
valid: false,
isValidated: true
}
},
submitting: false,
submitError: undefined
}
{
...
submitting: true,
submitError: undefined
}
{
inputs: {
email: {
value: "farskid@gmail.com",
valid: true,
isValidated: true,
}
},
submitting: false,
submitError: "Incorrect password."
}
{
submitting: boolean;
submitError: string | undefined;
}
submitting: false, error: undefined
submitting: true, error: undefined
submitting: false, error: "Error"
submitting: false, error: undefined
Editing
Submitting
Failed
Succeeded
{
submitting: boolean;
submitError: string | undefined;
isSuccess: boolean;
}
Same state object, different views
function handleSubmit(e) {
e.preventDefault();
const canSubmit = Object.values(state.inputs)
.map(v => v.valid)
.every(v => v === true);
if (!canSubmit) {
return;
}
}
Avoid mutually exclusive behaviors
from happening simultaneously
Avoid transitioning to the impossible states
Scene #3
{value: ""}
{value: "fars"}
{value: ""}
{value: "farskid@gmail.com"}
Type
InputState = "Empty" | "Invalid" | "Valid"
function transition(state, event) {
switch(state) {
case "Empty":
case "Invalid":
case "Valid":
switch(event.type) {
case "TYPE":
return validateEmail(event.payload)
? "Valid" : "Invalid"
default:
return state;
}
default:
throw Error("Impossible state")
}
}
onChange(e) {
setInputContext(e.target.value)
setInputState(transition(inputState, "TYPE"))
}
<input
type="text"
onChange={e => {
setInputContext(e.target.value);
setInputState(
transition(inputState, "TYPE")
);
}}
/>;
{
inputState === "Invalid" &&
<p>Enter a valid email address</p>;
}
{
!valid && isValidated &&
<p>Enter a valid email address</p>;
}
type State =
| {
formState:
| "Valid" | "Submitting"
| "SubmitSucceeded" | "SubmitFailed"
InputStates: {
email: "Valid"
password: "Valid"
};
}
| {
formState: "Invalid";
inputStates: {
email: "Empty" | "Invalid" | "Valid"
password: "Empty" | "Invalid" | "Valid"
};
};
type FormContext = {
email: string
password: string
submitError:
| Error
| undefined
}
<button
type="submit"
disabled={
state.formState === "Submitting"
}>
Sign In
</button>
{
formState.state === "SubmitFailed" &&
<p>{ formContext.submitError }</p>;
}
type State = "Opened" | "Closed"
type State =
| "Normal"
| "Hovered"
| "Active.Idle"
| "Active.Focused"
| "Disabled"
type State =
| "Min"
| "Mid"
| "Max"
const context = {
min: number,
max: number,
value: number
}
type Promise =
| Pending
| Settled.Fulfilled
| Settled.Rejected
const Context = {
resolvedValue?: any;
rejectedError?: any
}
Scene #4
David HAREL (1987):
A VISUAL FORMALISM FOR COMPLEX SYSTEMS*
Released
GRAB
Grabbed
MOVE
Dragging
MOVE
shiftX
shiftY
PageY
pageX
RELEASE
onMouseDown = () => {
sendEvent({ type: "GRAB", data: { shiftX, shiftY } });
}
onMouseMove = () => {
sendEvent({
type: "MOVE",
data: { x: event.pageX, y: event.pageY }
});
}
onMouseUp = () => {
sendEvent("RELEASE");
};
box.onmousedown = function(event) {
// (1) prepare to moving: make absolute and on top by z-index
box.style.position = 'absolute';
box.style.zIndex = 1000;
// move it out of any current parents directly into body
// to make it positioned relative to the body
document.body.append(box);
// ...and put that absolutely positioned ball under the pointer
moveAt(event.pageX, event.pageY);
// centers the ball at (pageX, pageY) coordinates
function moveAt(pageX, pageY) {
box.style.left = pageX - box.shiftX + 'px';
box.style.top = pageY - box.shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// (2) move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// (3) drop the ball, remove unneeded handlers
box.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
box.onmouseup = null;
};
};
{
initial: "released",
context: {
shiftX: 0,
shiftY: 0,
pageX: 0,
pageY: 0
},
states: {
released: {
on: {
GRAB: {
target: "grabbed"
}
}
},
grabbed: {
entry: [
"saveShiftPoints",
"saveBoxPositions",
"prepareBoxStyles",
"moveBox"
],
on: {
MOVE: "dragging"
}
},
dragging: {
entry: [
"saveBoxPositions",
"moveBox"
],
on: {
MOVE: "dragging",
RELEASE: "released"
}
}
}
}
Scene #5
{
initial: "released",
context: {
shiftX: 0,
shiftY: 0,
pageX: 0,
pageY: 0
},
states: {
released: {
on: {
GRAB: {
target: "grabbed"
}
}
},
grabbed: {
entry: [
"saveShiftPoints",
"saveBoxPositions",
"prepareBoxStyles",
"moveBox"
],
on: {
MOVE: "dragging"
}
},
dragging: {
entry: [
"saveBoxPositions",
"moveBox"
],
on: {
MOVE: "dragging",
RELEASE: "released"
}
}
}
}
Box is released at first
When it's released, it can be grabbed
As soon as it's grabbed, we remember mouse position and box position, prepare its styles and move it.
When it's grabbed, it can move
As soon as it's moving, we update its current position and move it.
When it's moving, it can be released
As long as it's moving, we keep moving it which means continuously updating its position.
{
initial: "A",
states: {
A: {},
B: {}
}
}
statechart.withConfig({
actions: {},
services: {},
delay: {}
})
Scene #6
xstate
@xstate/react
@xstate/test
@xstate/graph
@xstate/fsm
Slides at:
HolyJS Moscow 2019
By Farzad Yousefzadeh
A talk about explicitly thinking about states of GUI components. Implicit vs Explicit state, finite vs infinite state, logic modeling and logic visualization.
Lead developer @epicgames. Core @statelyai. Coach @mentorcruise. ❤️ Statecharts, Finite State Machines, and XState.