@davidkpiano Β· Craft Conf 2019
π
π
π
π
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
state + event = nextState
β
+ effects
β
event = nextState
β
// ...
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
});
}
// ...
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
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
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
idle
loading
success
failure
FETCH
RESOLVE
ERROR...?
Error
RETRY
Idle
Searching...
onEntry / prefetchResources
onEntry / fetchResults
Search
[query.length > 0]
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
A
B
C
D
E
A β B
A β B β C
A β D
A β 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
A
B
C
D
E
1. Abstract model
2. Transition analytics
3. Identify adaptive paths
4. Use analysis for adaptation
Photo by NEW DATA SERVICESΒ on Unsplash
Success
Signing in
Error
0.9
0.1
Login
Gallery
Profile
Camera
SUCCESS
TAP PROFILE
TAP CAMERA
BACK
BACK
Scroll
πΊ 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
+10
Gallery
Ads interspersed
Ads shown at top
Ad
Scroll
Scroll
TAP AD
+10
-1
-10
INTERSP > .5
TOP > .5
Contextual data
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
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
β
Statecharts
FSMs
Bottom-up
States & logic
Code Complexity
@davidkpiano Β· Craft Conf 2019