class GoatButton extends Component {
state = {
goat: undefined
};
fetchGoat = () => {
fetch('url/to/goat')
.then(goat => this.setState({ goat }));
}
render() {
const { goat } = this.state;
return (
<div>
{goat ? <img src={goat} /> : null}
<button onClick={this.fetchGoat}>
Fetch Goat
</button>
</div>
);
}
}
Set goat in state
Handle fetching the goat
Render goat button and image
state = {
goat: undefined,
goatError: false,
goatSearching: false
};
Add boolean flags
fetchGoat = () => {
if (this.state.goatSearching) return;
this.setState({
goatSearching: true,
goatError: false
});
fetch('url/to/goat')
.then(goat => this.setState({
goat,
goatSearching: false
}))
.catch(err => this.setState({ goatError: true });
}
Don't search if already searching
Indicate an error
render() {
const { goat, goatError, goatSearching } = this.state;
return (
<div>
{goat && <img src={goat} />}
{goatError && <span>Error: goat not found</span>}
<button onClick={this.fetchGoat} disabled={goatSearching}>
{goatSearching
? 'Searching...'
: goatError
? 'Goat fail, retry?'
: 'Fetch goat'
}
</button>
</div>
);
}
Disable button if loading
Custom button text
class GoatButton extends Component {
state = {
goat: undefined
};
fetchGoat = () => {
fetch('url/to/goat')
.then(goat => this.setState({ goat }));
}
render() {
const { goat } = this.state;
return (
<div>
{goat ? <img src={goat} /> : null}
<button onClick={this.fetchGoat}>
Fetch Goat
</button>
</div>
);
}
}
class GoatButton extends Component {
state = {
goat: undefined,
goatError: false,
goatSearching: false
};
fetchGoat = () => {
if (this.state.goatSearching) return;
this.setState({
goat: undefined,
goatSearching: true,
goatError: false
});
fetch('url/to/goat')
.then(goat => this.setState({ goat, goatSearching: false }))
.catch(err => this.setState({ goatError: true });
}
render() {
const { goat, goatError, goatSearching } = this.state;
return (
<div>
{goat && <img src={goat} />}
{goatError && <span>Error: goat not found</span>}
<button onClick={this.fetchGoat} disabled={goatSearching}>
{goatSearching
? 'Searching...'
: goatError
? 'Goat fail, retry?'
: 'Fetch goat'
}
</button>
</div>
);
}
}
Before
Aw π©
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
Number of possible states
Predetermined sequence
State + action = next state, always
PILL
TIMER
EAT
REVIVE
An initial state
A finite number of states
Transitions between states
Actions that cause transitions
CLICK
RESOLVE
REJECT
CLICK
CLICK
idle -> loading
loading -> goat -> error
error -> loading
goat -> loading
const machine = {
idle: {
CLICK: 'loading'
},
loading: {
RESOLVE: 'goat',
REJECT: 'error',
},
goat: {
CLICK: 'loading'
},
error: {
CLICK: 'loading'
}
};
const initialState = 'idle';
const machine = {
// ...
};
const initialState = 'idle';
function transition(state, action) {
return machine[state][action];
}
const t=(m,s,a)=>m[s][a]
state = {
goatState: initialState, // 'idle'
goat: undefined
};
commands = {
loading: this.fetchGoat
}
Finite state
Data
Commands (side effects)
transition = (action) => {
const { goatState } = this.state;
const nextState = machine[goatState][action];
const command = this.commands[nextState];
this.setState({
goatState: nextState
}, command);
}
Transition function!
Next command
Set state, then exec command
fetchGoat = () => {
fetch('url/to/goat')
.then(goat => this.setState({ goat },
() => this.transition('RESOLVE')
))
.catch(_ => this.transition('REJECT'));
}
render() {
const { goat, goatState } = this.state;
const buttonText = {
idle: 'Fetch goat'
loading: 'Loading...',
error: 'Goat fail, retry?',
goat: 'Fetch another goat'
}[goatState];
return (
<div>
{goat && <img src={goat} />}
{goatState === 'error' && <span>Error: goat not found</span>}
<button
onClick={() => this.transition('CLICK')}
disabled={goatState === 'loading'}>
{buttonText}
</button>
</div>
);
}
Declarative rendering
oh god conference wifi
import { Machine } from xstate;
const machine = Machine({ ... });
const nextState = machine
.transition(currentState, action);
states:
green
on:
yellow
red
states:
TIMER
yellow
on:
TIMER
red
walk ...
wait ...
stop ...
on:
TIMER
green
lightMachine
.transition('red.stop', 'TIMER')
.toString();
// 'green'
lightMachine
.transition('yellow', 'TIMER')
.value;
// { red: 'walk' }
textMachine
.transition('bold.off', 'TOGGLE_BOLD')
.value;
// {
// bold: 'on',
// italics: 'off',
// underline: 'off',
// list: 'bullets'
// }