Tackle React component Complexity
using
Reactive StateCharts
Farzad Yousef Zadeh
@farzad_yz
Farzad Yousef Zadeh
✈ Aerospace engineer 🔭 Astrophysicist
Senior Software Engineer @

CurrentState
PreviousState
github.com/farskid
twitter.com/farzad_yz
🇫🇮
People have different definitions
for
Complexity
Complexity
A11y/U9y
External Actors
Styling
Cross Platform Sharing
Cross Browser Compatibility
Business Logic
Taking many parameters into account
Branching
Cyclomatic Complexity
Parameters
Cyclomatic Complexity
Cyclomatic
Complexity
Confusion
?
?
?
?
?
Confusion
Unpredictable
Behaviour
Complexity plays well with
Legacy Code
The Time
Writing the Software is EASY
Maintaining it is HARD
Let's talk about Behaviour
What is Behaviour really?
How the software is supposed to work without talking about how it's implemented
The user is supposed to write an email in the search input
The search input is supposed to validate the email
The search input is supposed to query emails from the server
The querying should happen when the user has stopped typing
When there is no email available, the user should see a sad panda picture
When there is some error in querying, show a red text describing what went wrong.
Behaviour
is
Abstract
Behaviour
Impl Specific
( Part of )
An Abstract Language
to describe
Behaviour
StateCharts
extends
Finite State Machines
quintuple
WTF?

State
State
Initial State
Event
Event
Event
Event
f(state, event) => state
Finite state Machine
What is StateChart then?
Make Nested machines
Make Parallel Machines
Conditional Transitions
Store Non-concrete state
&& data
Things can happen only if other things have happened before
Things can live independently in the same context
Guard against invalidity / Branching logic
Model non-concrete concepts e.g. Animations / Internal data store
Submit form only after the input is validated
Twitter Feed vs Side Widget
Red input when invalid / Green input when valid
Battery percentage / List of countries / Server Error
State Structure
What is StateChart then?
Support Entry Actions
Support Exit Actions
Actions to be executed when you enter a state (Good for initializing stuff)
Actions to be executed when you exit a state (Good for cleaning up)
Start off a fetch controller upon entering the pending state
Cancel the fetch upon exiting pending state
...More
Think in STATES
State ≠ State
type State = {
isLoading: boolean;
accounts: Account[];
error?: any;
};
type State1 =
| "Idle"
| "Loading"
| "Success"
| "Error";
We're used to
in the FSM world
React Component
State Machine
Your State management tool
(Hooks, Redux, Mobx, RXJS, ...)
1
2
3
4
Time to get Practical
We will use the XSTATE library for our examples
We will use the JSON format to define the behaviour
Serializable
No surprise
Be Aware
👏🎹 @davidkpiano
we will focus on Component level state.
Debouncing input
Example 1
<DebouncedInput onChange={console.log} delay={800} />
const [value, setValue] = useState("");
const [loading, setLoading] = useState(false);
function onChange(value) {
props.onChange(value);
setValue(value);
}
useEffect(() => {
if (value) {
setLoading(true);
try {search(value)} catch(err) {} finally {
setLoading(false);
}
}
}, [value])
return (
<>
<input
value={value}
onChange={e => debounce(onChange, props.delay)(e.target.value)}
/>
{loading && <Spinner />}
</>
);
const [state, sendEvent] =
useMachine(
inputMachine.withConfig({
delays: {
DEBOUNCE: props.delay
}
})
);
function onChange(value) {
props.onChange(value);
sendEvent({
type: "VALUE",
data: value
})
}
return (
<>
<input
value={value}
onChange={e => onChange(e.target.value)}
/>
{state.matches("search") && <Spinner />}
</>
);
{
initial: "waiting",
states: {
waiting: {
on: {
VALUE: "debouncing"
}
},
debouncing: {
entry: "updateValue",
on: {
VALUE: "debouncing",
},
after: {
DEBOUNCE: [
{
target: "waiting",
cond: "isValueEmpty"
},
{target: "search"}
]
}
},
search: {}
}
}
View React
Machine
Abstract
Delayed Transitions
V = F (S)

Debouncing input
Cancellation
Example 2
<DebouncedInput onChange={console.log} delay={800} />
const [value, setValue] = useState("");
function onChange(value) {
props.onChange(value);
setValue(value);
}
useEffect(() => {
if (value) {
const ctrl = new AbortController();
search(value, {signal: ctrl.signal});
}
return () => {
ctrl.abort();
}
}, [value])
return (
<input
value={value}
onChange={e => debounce(onChange, props.delay)(e.target.value)}
/>
);
{
initial: "waiting",
states: {
...same,
search: {
entry: assign({ctrl: () => new AbortController()}),
invoke: {
src: "searchService",
onDone: "",
onError: ""
},
exit: [
ctx => ctx.ctrl.abort(),
assign({ctrl: () => undefined})
],
on: {
VALUE: "debouncing"
}
}
}
}
makeNewController
Invoked promises
Initialize a controller
Clean up
Cancellation
cancelSearch
cleanupController
Noticed what happened to VIEW?

Cancellation
Mutual Exclusivity
Example 3
<Form onSubmit={} onValidating={} />
const [value, setValue] = useReducer(reducer, {
isValidating: false,
isSubmitting: false,
error: undefined,
touchedInputs: []
});
Form State
Extended State
Actual Data
isValidating: true, isSubmitting: true
isValidating: true, isSubmitting: false
isValidating: false, isSubmitting: true
isValidating: false, isSubmitting: false
Form is idle
Form is being submitted
Form is being validated
WTF ???
Impossible state
{
initial: "idle",
context: {error: undefined, touchedInputs: []},
states: {
idle: {
on: {
VALUE: {
target: "validating"
}
}
},
validating: {
entry: ["markChangedInputAsTouched", "updateChangedInputValue"],
on: {
VALUE: "validating",
'': [
{target: "valid", cond: "inputsAreValid"},
{target: "invalid"}
]
}
},
valid: {
on: {
VALUE: "validating",
SUBMIT: "submitting"
}
},
invalid: {
on: {
VALUE: "validating"
}
},
submitting: {
...
}
}
}
Form state is not a data anymore
SUBMIT only happens when validating is done and inputs are valid
Avoid Mutual exclusivity

Underestimating the complexity growth
Example 4
<Carousel items={[]} />

event: Prev
event: Next
States!
State: First
State: Middle
State: Last
{
initial: "first",
cursor: 1,
states: {
first: {
on: {
NEXT: {
target: "middle",
actions: "incrementCursor"
}
}
},
middle: {
on: {
NEXT: [
{target: "last", cond: "isNextItemTheLastItem", actions: "incrementCursor"},
{target: "middle", actions: "incrementCursor"}
],
PREV: [
{target: "first", cond: "isPrevItemTheFirstItem", actions: "decrementCursor"},
{target: "middle", actions: "decrementCursor"}
]
}
},
last: {
on: {
PREV: {
target: "middle",
actions: "decrementCursor"
}
}
}
}
}
Start off form the first item
State: First
State: Middle
State: Last

Custom Start Index
Feature Request
<Carousel items={[]} startIndex={2} />
{
initial: startIndex === 1 ? "first" : startIndex === items.length ? "last" : "middle",
cursor: startIndex,
states: {
first: {
on: {
NEXT: {
target: "middle",
actions: "incrementCursor"
}
}
},
middle: {
on: {
NEXT: [
{target: "last", cond: "isNextItemTheLastItem", actions: "incrementCursor"},
{target: "middle", actions: "incrementCursor"}
],
PREV: [
{target: "first", cond: "isPrevItemTheFirstItem", actions: "decrementCursor"},
{target: "middle", actions: "decrementCursor"}
]
}
},
last: {
on: {
PREV: {
target: "middle",
actions: "decrementCursor"
}
}
}
}
}
State: First
State: Middle
State: Last
getFirstState(startIndex)



<Carousel items={[]} startIndex={1} />
<Carousel items={[]} startIndex={2} />
<Carousel items={[]} startIndex={3} />
Cyclic Carousels
Feature Request
<Carousel items={[]} startIndex={2} cyclic />
{
first: {
on: {
NEXT: {
target: "middle",
actions: "incrementCursor"
},
PREV: {
target: "last",
cond: "isCyclic",
actions: "setCursorToLast"
}
}
},
middle: {
...middle
},
last: {
on: {
NEXT: {
target: "first",
cond: "isCyclic",
actions: "setCursorToFirst"
},
PREV: {
target: "middle",
actions: "decrementCursor"
}
}
}
};
State: First
State: Middle
State: Last

Directions (LTR & RTL)
Feature Request
<Carousel items={[]} startIndex={2} cyclic dir="ltr" />
First State
Next
(LTR)
Prev
(RTL)
Cyclic ❌
Cyclic ✅
Combinatorial Explosion
Rapid growth of a problem based on how the combinators of the problem are affected by the input
{
first: {
on: {
NEXT: [
{target: "last", cond: "isCyclic & isRtl", actions: "setCursorToLast"},
{
target: "middle",
actions: "incrementCursor"
}
],
PREV: [
{target: "middle", cond: "isRtl", actions: "incrementCursor"},
{
target: "last",
cond: "isCyclic",
actions: "setCursorToLast"
}
]
}
},
...
};
State: First
State: Middle
State: Last

Autoplay
Feature Request
<Carousel
items={[]} startIndex={2} cyclic dir="rtl"
autoplay={2000}
/>
{
first: {
entry: send({type: "NEXT"}, {delay: "AUTOPLAY", id: "autoPlayEvent"}),
exit: cancel("autoPlayEvent"),
on: {
NEXT: [
{target: "last", cond: "isCyclic & isRtl", actions: "setCursorToLast"},
{
target: "middle",
actions: "incrementCursor"
}
],
PREV: [
{target: "middle", cond: "isRtl", actions: "incrementCursor"},
{
target: "last",
cond: "isCyclic",
actions: "setCursorToLast"
}
]
}
},
...
};
State: First
State: Middle
State: Last
That's it
carouselMachine.withConfig({
delays: {
AUTOPLAY: props.autoplay
}
})

It's here

Pause and Resume
Feature Request
<Carousel items={[]} startIndex={2} cyclic />
{
paused: { id: "paused", on: { PLAY: "#playing"} },
playing: {
first: {
after: {
AUTOPLAY: {
actions: send("NEXT"),
},
},
on: {
PAUSE: "#paused",
...
},
},
...
},
};
State: First
State: Middle
State: Last
Pause with state
Gets back to Playing
But it Resets!
{
paused: { id: "paused", on: { PLAY: "#playing.hist"} },
playing: {
hist: {
type: "history",
history: "deep"
},
first: {
after: {
AUTOPLAY: {
actions: send("NEXT"),
},
},
on: {
PAUSE: "#paused",
...
},
},
...
},
};
State: First
State: Middle
State: Last
History States!

Animation!
What We missed
{
paused: {},
playing: {
transitioning: {
after: {
TRANSITION_DELAY: "#waiting.hist"
}
},
waiting: {
hist: {},
first: {},
middle: {},
last: {},
},
},
};
function HeadlessCarousel(props) {
const {children, ...carouselProps} = props;
const [state, sendEvent] = useMachine(carouselProps);
return children({
state: state.value,
data: state.context,
next() {
sendEvent("NEXT")
},
prev() {
sendEvent("PREV")
},
play() {
sendEvent("PLAY");
},
pause() {
sendEvent("PAUSE")
}
})
}
<HeadlessCarousel>
{headlessCarousel => {
<div>
<button onClick={headlessCarousel.next}>
Next
</button>
</div>
}}
</HeadlessCarousel>
<HeadlessCarousel>
{headlessCarousel => {
<View>
<TouchableOpacity onPress={headlessCarousel.next}>
Next
</TouchableOpacity>
</View>
}}
</HeadlessCarousel>
stdin.on("keypress", (_, code) => {
if (code === ARROW_RIGHT) {
sendEvent("NEXT");
}
});
Web
Mobile
Command Line 😱
State Machines
are
Implementation Details
End user doesn't care if you're using state machines
End User has a more flattened perception
of
Your App
Use state machines to model
Let this model be used by your choice of tech
Test like you don't have state machines
Use this model as a means of communication
Thank You!
Grazie!
Farzad Yousef Zadeh
@farzad_yz