@davidkpiano Β· Craft Conf 2019
State-of-the-Art User Interfaces
with State Machines
π
π
π
π
Fetch data
π
GET api/users
Pending
200 OK
Fetch data
π
GET api/users
Pending
200 OK
Fetch data
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
Fetch data
π
GET api/users
Pending
200 OK
Fetch data
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
Fetch data
The most neglected variable is time.
state + event = nextState
β
+ effects
β
event = nextState
β
π
π
ππ€
π °οΈβοΈ
π °οΈποΈββοΈ
π
π °οΈ
β½οΈπΎπ±
Callbacks
Promises
Async-Await
Observables
// ...
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 => {
if (this.state.canceled) {
return;
}
// mi a faszΓ©rt csinΓ‘lod ezt?
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
.catch(error => {
if (this.state.canceled) {
return;
}
// mi a faszΓ©rt csinΓ‘lod ezt?
this.setState({
loading: false,
error: true
});
});
}
onCancel() {
this.setState({
loading: false,
error: false,
canceled: true
});
}
// ...
π π
The bottom-up
approach
someButton.addEventListener('click', e => {
});
π€ LOGIC
π€ π€ MORE LOGIC
πΌ BUSINESS LOGIC
π» PARTY LOGIC
βοΈ CONDITIONAL LOGIC
π₯ SIDE EFFECT
π UPDATE STATE
π UPDATE GLOBAL STATE
β± AWAIT... UPDATE STATE
π£ ANOTHER SIDE EFFECT
π // TODO: TEST
π π
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
How can we model the behavior
of user interfaces?
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
Fulfilled
Rejected
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
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' }
}
}
};
Define transitions between
states & events
function transition(state, event) {
return machine
.states[state] // current state
.on[event] // next state based on event
|| state; // fallback to current state
}
Transition function determines
next state from state + event
Software bugs are
made visually clear
idle
loading
success
failure
FETCH
RESOLVE
ERROR...?
Error
RETRY
Does this scale?
Statecharts
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
Bold ON
Bold OFF
Italics ON
Italics OFF
Underline ON
Underline OFF
Characters
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
Searched
npm install xstate
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' }
}
}
};
import { Machine } from 'xstate';
const machine = Machine({
initial: 'idle',
states: {
idle: {
on: { SEARCH: 'searching' }
},
searching: {
on: {
RESOLVE: 'success',
REJECT: 'failure',
SEARCH: 'searching'
}
},
success: {
on: { SEARCH: 'searching' }
},
failure: {
on: { SEARCH: 'searching' }
}
}
});
π¦ Hierarchical states
π―ββοΈ Parallel states
π₯ Actions (declarative side effects)
π° History states
β Conditional transitions
π External state (context)
β± Delayed transitions and events
π©βπ« Interpreter
π Final states
π Invoking external machines
π New visualization and simulation tools
β€οΈ And much more
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
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
A
B
C
D
E
Determinism
Visualization
π©βπ» Developer Experience
Communication
Analytics
Simulation
Testability
π¨βπ©βπ§βπ¦ User Experience
1. Abstract model
2. Transition analytics
3. Identify adaptive paths
4. Use analysis for adaptation
Adaptive User Interfaces
Photo by NEW DATA SERVICESΒ on Unsplash
Success
Signing in
Error
0.9
0.1
Weighted graphs
Login
Gallery
Profile
Camera
SUCCESS
TAP PROFILE
TAP CAMERA
BACK
BACK
Scroll
Analysis of transitions
πΊ Login, Gallery, Gallery
πΊ Login, Gallery, Camera
πΊ Login, Gallery, Profile
0.90, 0.31
0.90, 0.57
0.90, 0.12
0.90
0.31
0.57
0.13
UNAUTHORIZED
0.10
SUCCESS
(decide)
TAP AD
Analysis of actions
+10
Gallery
Ads interspersed
Ads shown at top
Ad
Scroll
Scroll
TAP AD
+10
-1
-10
INTERSP > .5
TOP > .5
Decision trees
Contextual data
- Registration date
- Number of friends
- Posting frequency
- Location
- ...etc.
New user?
< 30 days
>= 30 days
Show ad at top
Friend count?
< 100
>= 100
Show more ads
Show less ads
All data
A
B
C
D
E
import { Machine } from 'xstate';
const myMachine = Machine({
// ...
});
const myService = interpret(myMachine)
.onTransition(state => {
analytics.track({
target: state.value,
source: state.history
? state.history.value
: undefined,
event: sanitize(event),
timestamp: Date.now()
});
})
.start();
A
B
C
D
E
import { Machine } from 'xstate';
import { getShortestPaths } from '@xstate/graph';
const myMachine = Machine({
// ...
});
const shortestPaths = getShortestPaths(myMachine);
-
A: A
-
B: A -> B
-
C: A -> B -> C
-
D: A -> D
-
E: A -> D -> E
Agent
Environment
Action
State
Reward
Reinforcement
learning
service.onTransition(state => {
analytics.track({
source: state.history.value,
target: state.value,
context: state.context,
event: state.event,
timestamp: Date.now()
})
});
Application
Executable Model
Metrics Tracking
Adaptive Transitions
Adaptive UI
β
The future of state
is nothing new.
Advantages
of using statecharts
- Visualized modeling
- Precise diβagrams
- 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)
Statecharts
FSMs
Bottom-up
Complexity
trade-offs
States & logic
Code Complexity
The world of statecharts
Make your code do more.
Thank you Craft Conf!
@davidkpiano Β· Craft Conf 2019
State of the Art User Interfaces with State Machines
By David Khourshid
State of the Art User Interfaces with State Machines
Craft Conf 2019