The art of
explicit and consistent user interfaces


Farzad Yousef Zadeh
Senior software engineer
Former Aerospace engineer 🚀 and astrophysicist 🌌
Implicit vs Explicit state
Finite state vs Infinite state
-
Decoupled from implementation
-
ported to several platforms
logic
-
Modeled
We will talk about
-
Visualized
The What
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
}
}
})
One purpose
multiple approaches
State management tools
are containers
deal with the architecture (flux, push based, pull based)
tell you how to store states (single source, distributed)
tell you how to update states (Mutable, immutable, observable)
source: http://tiny.cc/eg2hfz
Current state flow
After adding modeling layer


A flashback to
GUI history
GUI is event-driven
Alan Kay, the Dynabook, 1972
GUI is built upon events and messages
Listen to events and execute actions (side effects)
event + action paradigm

The problem
Scene #2
facetime bug



MS calculator bug
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
What was wrong with those then?
Implicitly handling the state
Event handlers
Mutations
Side effects
Asynchrony
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
}
No value
Invalid
no validation error
A single input with IMPLICIT state

{
value: "fars",
valid: false
}
with value
Invalid
validation error shown
A single input with IMPLICIT state

{
value: "",
valid: false
}
No value
Invalid
validation error shown
A single input with IMPLICIT state

{
value: "",
valid: false
}

{
value: "",
valid: false
}
uh Oh!

{
value: "",
valid: false,
isValidated: false
}
No value
Invalid
no validation error
A single input with IMPLICIT state
{
value: "fars",
valid: false,
isValidated: true
}
with value
Invalid
validation error shown
A single input with IMPLICIT state

{
value: "",
valid: false,
isValidated: true
}
No value
Invalid
validation error shown
A single input with IMPLICIT state

{
value: "farskid@gmail.com",
valid: true,
isValidated: true
}
with value
valid
no validation error
A single input with IMPLICIT state

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
2
2
2
4

valid states:
4
impossible states:
impossible states
v.length
valid
isValidated
with bad modeling,
your code complexity grows faster than the domain complexity
with impossible states,
You need to test cases that won't event happen in real life
An impossible state is where you tell users to restart
Add guards to avoid impossible states
function handleChange(newValue) {
if (!isValidated) {
setValidated(true);
}
if (newValue.length === 0 || !validateEmail(newValue)) {
setValid(false);
} else {
setValid(true);
}
setValue(newValue);
}
Guards increase
cyclomatic complexity

Number of independent paths a program can take
More guards:
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
}
embed input in form

{
inputs: {
email: {
value: "fars",
valid: false,
isValidated: true
}
},
submitting: false,
submitError: undefined
}
embed input in form

{
inputs: {
email: {
value: "",
valid: false,
isValidated: true
}
},
submitting: false,
submitError: undefined
}
embed input in form

{
...
submitting: true,
submitError: undefined
}
{
inputs: {
email: {
value: "farskid@gmail.com",
valid: true,
isValidated: true,
}
},
submitting: false,
submitError: "Incorrect password."
}

{
submitting: boolean;
submitError: string | undefined;
}
Impossible states again!
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
Adding Guards
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
The Solution
Scene #3
Discovering Finite states
Thinking about states explicitly
Think in states explicitly
in the input example




Empty
Finite state
inFinite state
{value: ""}
Invalid
{value: "fars"}
Invalid
{value: ""}
{value: "farskid@gmail.com"}
Valid
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>;
}
conditional rendering based on finite state
{
!valid && isValidated &&
<p>Enter a valid email address</p>;
}
Think in states explicitly in the form example
Finite state
inFinite state
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>;
}
conditional rendering based on finite state
Tooltip/modal/dropdown
type State = "Opened" | "Closed"
Button
type State =
| "Normal"
| "Hovered"
| "Active.Idle"
| "Active.Focused"
| "Disabled"
Range Input / Progress
type State =
| "Min"
| "Mid"
| "Max"
const context = {
min: number,
max: number,
value: number
}
Elements have finite states

type Promise =
| Pending
| Settled.Fulfilled
| Settled.Rejected
const Context = {
resolvedValue?: any;
rejectedError?: any
}
Time based side effects have finite states

Scaling
Scene #4
Making modeling practical
Define states explicitly
Separate finite and infinite states
abstract with focus on logic
Reduce guards and cyclomatic complexity
capable of modeling complex GUI
statecharts

David HAREL (1987):
A VISUAL FORMALISM FOR COMPLEX SYSTEMS*
Extends FSM model
States with relations
Several places to run side effects
Several types of side effects
state + event =>
next state + side effects

Statechart implementation library

Xstate
Based on SCXML
JSON definition
Built-in Visualizor

Rewrite the input
in statecharts




Interactions in statecharts
modeling a Dragging box
Interactions with statecharts
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");
};
Event listeners and statecharts
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"
}
}
}
}
Before
AFTER
Additional benefits
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.
Statecharts read like English
Statecharts visualize logic
Generate directed Graph
Showcase state paths
Paths can be used by QA team to test for edge cases
cross competence teams
onboarding

Statecharts visualization in pull requests

Statecharts decouple logic from implementation
Abstract declarative JSON
{
initial: "A",
states: {
A: {},
B: {}
}
}
Implementation
statechart.withConfig({
actions: {},
services: {},
delay: {}
})
PLatform api
Next time someone asked how hard can it be?
Answer: let's draw its statecharts and see!
RECAP
Scene #6
levels solution complexity and problem complexity
Recap
finite state vs infinite state
statecharts for practical modeling complex applications
statecharts for knowledge sharing and communication
Implicit vs explicit state management
A new modeling layer
make impossible states, impossible
Avoid mutual exclusivity problems
THINK in STATES
Check these out
The World of statecharts

xstate
@xstate/react
@xstate/test
@xstate/graph
@xstate/fsm



Thank you!
Спасибо!
Slides at:
HolyJS Moscow 2019