David Khourshid Β· @davidkpiano Β· Full Stack Fest 2018
Microsoft
PequeΓ±oflojo
Fetch data
GET api/users
Pending
200 VALE
Fetch data
GET api/users
Pending
200 VALE
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 VALE
Fetch data
GET api/users
Pending
GET api/users
Pending
GET api/users
Pending
GET api/users
Pending
Fetch data
// ...
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 => {
// quΓ© carajo
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 => {
// quΓ© carajo
if (this.state.canceled) {
return;
}
this.setState({
loading: false,
error: true
});
});
}
EVENT
State
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
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' }
}
}
};
function transition(state, event) {
return machine.states[state].on[event];
}
Define transitions between
states & actions
Transition function determines
next state from state + event
const machine = {
// ...
};
function transition(state, event) {
return machine.states[state].on[event];
}
Store the current state
In event handlers, only send events
let currentState = machine.initial;
function send(event) {
currentState = transition(currentState, event);
}
// ...
someButton.addEventListener('click', () => {
send('SEARCH');
});
Create a function for
dispatching (sending) events
Any sufficiently complicated model class contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of a state machine.
state management library
A
B
C
D
E
A β B
A β B β C
A β D
A β D β E
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
transition(currentState, event) {
const nextState = // ...
Telemetry.sendEvent(
currentState,
nextState,
event
);
return nextState;
}
A
B
C
D
E
idle
loading
success
failure
FETCH
RESOLVE
ERROR...?
Error
RETRY
Idle
Searching...
onEntry / prefetchResources
onEntry / fetchResults
Search
[query.length > 0]
onExit / cancelSearch
H
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
npm install xstate --save
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'
}
}
}
});
import { scan } from 'rxjs/operators';
const machine = new Machine({/* ... */});
const event$ = // ...
const state$ = event$.pipe(
scan(machine.transition),
tap(({ actions }) => {
// execute actions
});
const machine = new Machine({/* ... */});
export const reducer = machine.transition;
// ...
class App extends Component() {
// ...
componentDidUpdate() {
const { state } = this.props;
const { actions } = state;
const nextState = actions.reduce(action => {
// execute the action commands
}, this.state);
// local state, or send it to Redux
this.setState(nextState);
}
// ...
}
machine.transition() is
just a reducer function!
Learning curve
Modeling requires planning ahead
Statecharts
FSMs
Bottom-up
States & logic
Code Complexity
BETA!
npm install xstate@next
π External state (context)
β± Delayed transitions and events
π©βπ« Interpreter
πΎ SCXML conversion
BETA!
π New visualization and simulation tools
// ...
on: {
INC: [{
actions: [assign({
count: ctx => ctx.count + 1
})]
}]
}
// ...
state.context;
// {
// count: 42
// }
// ...
// After 1 second, go to 'yellow' state
after {
1000: 'yellow'
}
// ...
import { Machine } from 'xstate';
import { interpret }
from 'xstate/lib/interpreter';
const machine = Machine(...);
// Create interpreter
const interpreter = interpret(machine);
// Add listener
interpreter
.onTransition(currentState => {
// Listen to state updates
this.setState({ currentState });
});
// Initialize interpreter
interpreter.init();
// Send events!
interpreter.send('SOME_EVENT');
π Final states
// ...
crosswalk1: {
initial: 'walk',
states: {
walk: {
on: { PED_WAIT: 'wait' }
},
wait: {
on: { PED_STOP: 'stop' }
},
stop: {
type: 'final'
}
}
},
crosswalk2: {
initial: 'walk',
states: {
walk: {
on: { PED_WAIT: 'wait' }
},
wait: {
on: { PED_STOP: 'stop' }
},
stop: {
type: 'final'
}
}
}
// ...
David Khourshid Β· @davidkpiano Β· Full Stack Fest 2018