@davidkpiano · JSConf Iceland 2018
Simplifying Complex UIs
with Finite Automata & Statecharts
New APIs
#Frameworkless
If software is music,
developers are composers
What the different parts are
How these parts are composed together
How things change over time
What is the "music notation"
of user interfaces?
API
Human
-
documented
-
predictable
-
testable
-
un
-
un
-
un
- > 50.000 software engineers
- > 7 designers
- > 1,5 billion customers
Dear Microsoft Teams, please fix this ☝️
We don't develop software
for just 1 person
happy path
// ...
onSearch(query) {
fetch(FLICKR_API + '&tags=' + query)
.then(data => this.setState({ data }));
}
// ...
Show data when results retrieved
// ...
onSearch(query) {
this.setState({ loading: true });
fetch(FLICKR_API + '&tags=' + query)
.then(data => {
this.setState({ data, loading: false });
});
}
// ...
Show loading screen
Show data when results retrieved
Hide loading screen
// ...
onSearch(query) {
this.setState({ loading: true });
fetch(FLICKR_API + '&tags=' + query)
.then(data => {
this.setState({ data, loading: false });
})
.catch(error => {
this.setState({
loading: false,
error: true
});
});
}
// ...
Show loading screen
Show data when results retrieved
Hide loading screen
Show error
Hide loading screen
// ...
onSearch(query) {
this.setState({
loading: true,
error: false
});
fetch(FLICKR_API + '&tags=' + query)
.then(data => {
this.setState({
data,
loading: false,
error: false
});
})
.catch(error => {
this.setState({
loading: false,
error: true
});
});
}
// ...
Show loading screen
Show data when results retrieved
Hide loading screen
Show error
Hide loading screen
Hide error
Hide error
// ...
onSearch(query) {
if (this.state.loading) return;
this.setState({
loading: true,
error: false,
canceled: false
});
fetch(FLICKR_API + '&tags=' + query)
.then(data => {
if (this.state.canceled) {
return;
}
this.setState({
data,
loading: false,
error: false
});
})
.catch(error => {
// fokking fokk
if (this.state.canceled) {
return;
}
this.setState({
loading: false,
error: true
});
});
}
onCancel() {
this.setState({
loading: false,
error: false,
canceled: true
});
}
// ...
Show loading screen
Show data when results retrieved
Hide loading screen
Show error
Hide loading screen
Hide error
Hide error
Search in progress already
Cancel cancellation
Ignore results if cancelled
Ignore error if cancelled
Cancel search
error: false
});
})
.catch(error => {
// fokking fokk
if (this.state.canceled) {
return;
}
this.setState({
loading: false,
error: true
});
});
}
Spaghetti code
Lasagna code
🍑 🆙
The bottom-up
EVENT
- ACTION 1
- ACTION 2
- ACTION 3
- ACTION 4
- ACTION 5
- ACTION 6
approach
State
🍑 🆙
code
Difficult to understand
Difficult to test
Will contain bugs
Difficult to enhance
Features make it worse
Source: Ian Horrocks, "Constructing the User Interface with Statecharts", ch. 3 pg. 17
Intuition
- UI components are not independent
- Actions are based on event & state
- The event-action paradigm is too simple
Finite state machines
and statecharts
Finite state machines
-
have one initial state
-
a finite number of states
-
a finite number of events
-
a mapping of state transitions
triggered by events -
a finite number of final states
Idle
Pending
Rejected
Fulfilled
Fetch
Resolve
reject
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
SEARCH
const machine = {
initial: 'idle',
states: {
idle: {
on: { SEARCH: 'searching' }
},
searching: {
on: {
RESOLVE: 'success',
REJECT: 'failure',
SEARCH: 'searching'
}
},
success: {
on: { SEARCH: 'searching' }
},
failure: {
on: { SEARCH: 'searching' }
}
}
};
function transition(state, event) {
return machine.states[state].on[event];
}
Define transitions between
states & actions
Transition function determines
next state from state + event
State machines in
VS Live Share
Signed out
Signing in
Signed in
SIGN IN
SIGN IN SUCCESS
SIGN IN FAILURE
Sharing...
Shared
Joining...
Joined
share
share Success
Join Success
Join
Leave
End Collab session
Sign in
Using state machines
for analytics
transition(currentState, event) {
const nextState = // ...
Telemetry.sendEvent(
currentState,
nextState,
event
);
return nextState;
}
Using state machines
for integration testing
- Shortest path algorithms (Dijkstra, Bellman-Ford, A* search, etc.)
- Analytics provides weights
- Represents all happy paths
- Can be automatically generated
A
B
C
D
E
A → B
A → B → C
A → D
A → D → E
Using state machines
for integration testing
- Depth-first search (DFS) algorithm for finding all simple paths
- Represents all possible user flows
- Reveals all edge cases
- Can be automatically generated ⚠️
A
B
C
D
E
A → B
A → B → C
A → D
A → D → E
A → D → E → C
A → D → B → C
A → B → E → C
A → D → E → B → C
Software bugs are
made visually clear
A
B
X
Wrong state
E
E
Software bugs are
made visually clear
A
B
Unhandled event
E
E
??
Software bugs are
made visually clear
A
B
Missing transition
???
C
D
E1
E2
Software bugs are
made visually clear
A
B
Missing states
Loading...
E
Success
Software bugs are
made visually clear
A
B
E1
C
E2
E2
Race condition
E1
Harel Statecharts
extended finite state machines
Statecharts
Idle
Searching...
onEntry / prefetchResources
onEntry / fetchResults
Search
- Actions - onEntry, onExit, transition
- Guards - conditional transitions
[query.length > 0]
Statecharts
- Actions - onEntry, onExit, transition
- Guards - conditional transitions
- Hierarchy - nested states
- Orthogonality - parallel states
- History - remembered states
H
Statecharts
with xstate
npm install xstate --save
- Actions - onEntry, onExit, transition
- Guards - conditional transitions
- Hierarchy - nested states
- Orthogonality - parallel states
- History - remembered states
const lightMachine = Machine({
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
});
const nextState = lightMachine
.transition('green', 'TIMER');
// State {
// value: 'yellow'
// }
const lightMachine = Machine({
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
onEntry: ['activateYellow']
on: {
TIMER: 'red'
}
},
red: {
onExit: ['stopCountdown']
on: {
TIMER: 'green'
}
}
}
});
const lightMachine = Machine({
initial: 'green',
states: {
green: {
on: {
TIMER: {
yellow: {
cond: (xs, event) =>
event.elapsed > 10000
}
}
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
});
const lightMachine = Machine({
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
initial: 'walk',
states: {
walk: {
{ on: { PED_COUNTDOWN: 'wait' } }
},
wait: {
{ on: { PED_COUNTDOWN_END: 'stop' } }
},
stop: {}
}
}
}
});
const lightsMachine = Machine({
parallel: true,
states: {
northSouthLight: {
initial: 'green',
states: {
// ...
}
},
eastWestLight: {
initial: 'red',
states: {
// ...
}
}
}
});
const payMachine = Machine({
initial: 'method',
states: {
method: {
initial: 'card',
states: {
card: {
on: { SELECT_CASH: 'cash' }
},
cash: {
on: { SELECT_CARD: 'card' }
}
},
on: {
NEXT: 'review'
}
},
review: {
on: {
PREV: 'method.$history'
}
}
}
});
Bold ON
Bold OFF
Italics ON
Italics OFF
Underline ON
Underline OFF
Characters
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
SEARCH
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
Searched
The future
of xstate
- Improved developer ergonomics
- Full SCXML support and conversion
- A reactive interpreter
- Editable visualization tools
- More examples in many frameworks
Statecharts
FSMs
Bottom-up
Complexity
trade-offs
States & logic
Code Complexity
Advantages
of using statecharts
- Visualized modeling
- Precise diagrams
- Automatic code generation
- Comprehensive test coverage
- Accommodation of late-breaking requirements changes
Disadvantages
of using statecharts
Learning curve
Modeling requires planning ahead
Not everything can be modeled (yet)
Resources
- The World of Statecharts - Erik Mogensen
- Statecharts: A Visual Formalism for Complex Systems - David Harel (PDF)
- Constructing the User Interface with Statecharts - Ian Horrocks (book)
- xstate documentation
- How to model the behavior of Redux apps using statecharts - Luca Matteis
- React Automata - Michele Bertoli
- Pure UI - Guillermo Rauch
- Pure UI Control - Adam Solove
and tools
Write once, write anywhere
Learn once, write anywhere
Model once, implement anywhere
Let's improve the way we develop.
Takk JSConf Iceland!
@davidkpiano · JSConf Iceland 2018
Simplifying Complex UIs with Finite Automata & Statecharts
By David Khourshid
Simplifying Complex UIs with Finite Automata & Statecharts
JSConf Iceland 2018
- 11,876