
Simpler UI Logic With Finite State Machines - terodox
https://terodox.tech/ui-state-machines/
======
jsf01
I like the idea of using FSMs for UI logic a lot but this article barely even
scratched the surface on the topic. Their “state machine” is basically just a
switch statement with some useless boilerplate around it. If anyone’s
interested in the topic I’d recommend looking at the statecharts from overmind
([https://overmindjs.org/guides/intermediate/06_statecharts?vi...](https://overmindjs.org/guides/intermediate/06_statecharts?view=react&typescript=false))
and xstate which I believe was their inspiration
([https://xstate.js.org/](https://xstate.js.org/)).

~~~
DoctorOetker
I agree, I also think FSMs + 3D vector graphics (bezier surfaces etc) with 3rd
dimension for time, can replace a lot of javascript code, and any remaining
requirement for turing complete languages should be explicitly apps, instead
of websites, so the web can start clean up the javascript hogging and tracking
mess...

------
DecoPerson
I find "state" to be quite an overloaded term, especially in React land, so I
use "stage" instead for the various state machines in our applications. It's
caught on in our org and I've heard no complaints.

State machines are great but they get quite complicated when you have two
machines interacting at some point. I find this happens quite a bit for
complicated UIs, like a data grid with filter and search controls, in-row
controls, a preview pane on the side, etc. Things that can be reasonably
simple become overly complicated as you try to cram everything into a state
machines model.

It's hard to give concrete examples... but it happens often. I even find
myself doing it.

Rust enums (sum types) are really good for doing FSMs, as they:

\- Allow you to add fields to each state, which makes it easier to understand
what each state is for at a glance.

\- Prevent you from accessing fields for a different state at the wrong time.

\- Make it easy to do nested states, without that nestedness existing for
states that don't need it.

\- Provide exhaustiveness warnings for if you're not handling a case.

I do wish more languages had sum types.

~~~
bcherny
The number of possible states exploding is indeed a problem with frameworks
like React.

One mental model that helps reduce this load, that you alluded to, is avoiding
product types where possible in favor of sum types.

Eg. Imaging you have a type describing React props for some component: {a?:
number} and {a: number} | {} seem very similar on the surface — they both
describe an object with a single field that may or may not be defined. In both
cases you have 2 possible states.

But as soon as you add another field, the number of states can grow quickly
unless you’re thoughtful: {a?: number, b?: string} has 4 states, while the
model you might have been trying to describe — {a: number} | {b: string} —
still has just two.

The less states you have, the easier it is to reason about and automatically
verify your model. But in practice, people often accidentally introduce
product type where they meant to use sums. I see this with Flow and
TypeScript-typed React code all the time.

~~~
danenania
"{a?: number, b?: string} has 4 states, while the model you might have been
trying to describe — {a: number} | {b: string} — still has just two."

The problem I've run into with the second approach as the number of states
expand is 1 - you end up with tons of boilerplate to discriminate between
types, and 2 - it becomes hard to treat objects polymorphically.

I sometimes end up doing {a: number, b?: undefined} | {b: string, a?:
undefined} so that I can write `if (obj.a) doSomething()`, but it obviously
gets unwieldy with a lot of properties and states. Imo it would be nice if TS
did this automatically so that I could check for the existence of any property
that _might_ be defined on an object.

~~~
crdrost
Right, TS wants you to go full-on tagged union.

    
    
        {type: 'a', a: number} | {type: 'b', b: string}
    

When you do this you consume it with a switch(x.type) statement, not an if()
statement, so that you can write `default: assertImpossible(x.type)` for some
function `assertImpossible(y: never): never` that throws an exception of your
choice.

This gives clear fail-at-runtime semantics if JS hands you some crap your
types weren’t anticipating, but also fail-at-compile-time semantics if your
pattern match is no longer exhaustive (i.e. you added a new case, now
TypeScript tells you everything you need to change).

In general it's really nice to view any data structure as isomorphic to the
control structure that consumes it. “lists” are consumed by for..of loops,
“dicts” are consumed by for..of Object.keys() loops, sum types are consumed by
switches, objects are consumed by property accesses, etc.

~~~
bcherny
If your code is fully typed, you don't need the default, since you know at
compile time that you've handled your cases (Playground link: [0]):

    
    
        type Props =
          | { type: 'a', a: number }
          | { type: 'b', b: string }
    
        function f(props: Props): number {
          switch (props.type) {
            case 'a': return 1
            case 'b': return 2
          }
        }
    

Unfortunately you're right that this breaks when using `if` instead of
`switch` (unless you have a default return at the end of your function, or
replace one of your `if`s with an `else`; Playground link: [1]):

    
    
        type Props =
          | { type: 'a', a: number }
          | { type: 'b', b: string }
    
        // Error TS2366: Function lacks ending return statement and return
        // type does not include 'undefined'.
        function f(props: Props): number {
          if (props.type === 'a') {
            return 3
          }
          if (props.type === 'b') {
            return 4
          }
        }
    

This second case is a bug in TSC, and is tracked in
[https://github.com/microsoft/TypeScript/issues/18319](https://github.com/microsoft/TypeScript/issues/18319).

[0]
[https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgey...](https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgeygZ2AXgFClAH2AG9xoYAuYAcgEMqAaYGygOwFcBbAIxkWAF9ceYIRKRYlKlwbAulNGEQBLFgHMB2ISABmbFgGMwS5C2DaAFFBTpKSVGgCUrTjz5EtoNAHclYfQAtgS2s0ADpxGAdiDzx9GjQ4WipKRBgwNkRTAGYY0DiE6mkUtIzTABZcwTxBIA)

[1]
[https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgey...](https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgeygZ2AXgFClAH2AG9xoYAuYAcgEMqAaYGygOwFcBbAIxkWAF9ceYIRKRYlKlwbAulNGEQBLFgHMB2ISAD024AFFEKPgBUAygCYAzADYblAGJsWAYzBLkLYABsaLgNYYMCwAJirqiDBgbIheCjRgMBzBYEyhwJHRsVrAuqSwwCHIMBgsyKkqLt5sIXBUzrUAZiowIVQAdDmNzm4eXo0AFFAo6JRIqGgAlKycPHxEOSBKjcBDI2jt4nCYO9R0k8SLoJkxXlZHgsLAy6vDE5tkWLtSVAcLVyAnscAALBc5giAA)

------
fnordsensei
"If you haven't built a finite state machine, you've built a bad finite state
machine."

I've recently started using Fulcro (framework in ClojureScript), where they
are first class citizens in the form of "UI state machines."

[http://book.fulcrologic.com/#_ui_state_machines](http://book.fulcrologic.com/#_ui_state_machines)

In retrospect it's obvious how much better it is to contain, for example, the
logic of logging in and signing up within an FSM.

~~~
Jeff_Brown
> "If you haven't built a finite state machine, you've built a bad finite
> state machine."

Thank you!

I read the article, and then beyond, and I still don't know what in tarnation
distinguishes an FSM from a simple state. Every program has a set of states
and transitions. Is the difference simply that in an FSM the programmer has
modeled both sets explicitly? And if so, what is the model -- a map from
states to available transitions?

~~~
IlGrigiore
The main difference between a FSM and a statechart is the compositionality
aspect of the two formalisms. The FSM can be combined using the OR and the AND
operators: the AND operator produces a number of states that is equal to the
cartesian product of the original states of the two FSMs that are conjuncted.
This means that you are limited in the usefulness of the FSM by the number of
interactions between systems, because the number of states increases
exponentially.

The statechart solves this problem by using a representation that allows both
OR and AND avoiding the state explosion. Multiple states can be combined in a
super-state, which allows to model common properties of the enclosed states,
provided that the internal substates are XOR-ed, i.e. only one of them can be
active in a given time. The advantage of the super-states is that they allow
the specifier of the system to proceed in a top-down manner by specifying
iteratevily the complete behaviour of the system. These super-states can also
conjuncted, to model the interactions between the systems and to represent
their parallelism. This conjunction creates implicit interactions between the
two subsystems, which substitute the need to create the product FSM.

Both FSM and statecharts can represent the same behaviour of a system, but
they differ in how easily understandable their representation can be for
complex systems.

------
girishso
That looks a whole lot like Elm's Custom/Union/Sum types, except the compiler
helps by making `STATE.isValid` redundant (which the programmer might forget
to call).

~~~
masklinn
A proper sum type would go way beyond that because each state would only
contain (and have access to) the relevant data as well, whereas with a C-style
enum the data is "sibling" to the enum, and so you generally get possibly
garbage or irrelevant data in every state and the possibility for that garbage
to not be properly initialised or overridden on state transition.

And the compiler would further ensures that you are _handling_ every possible
state at every level.

One problem I've often encountered with UI state machines is the explosion in
error states, and the "proper" handling of invalid state transitons
(unexpected operations) not from a technical perspective but from a UI
perspective.

------
omneity
FSMs (and HSMs, the hierarchical version) are very useful for UI design, I am
afraid the point that the article is trying to make:

> A simpler approach would be to have a single variable for state, and only
> allows switching between well known states!

Was not reflected in the code, since it's possible to transition directly from
"error" to "success" without going through "submitted".

Which makes me wonder: Does anyone know whether there's existing art for
proving UIs using something like TLA+? Sounds like in interesting match, if
your UI is following a FSM pattern.

------
ketzo
At the last company I worked with, our React codebase was very heavy with
enums for exactly this reason (thank you Typescript). Anecdotally, it makes
reading a new codebase WAY easier: switch(state) is much easier to understand
than if (a bunch of flags).

------
rumanator
This is a good article to tell everyone that Qt extensively supports finite
state machines to simplify UI logic, to the point that it even allows
developers to use State Charts XML (SCXML) to CRUD finite state machines that
drive UI states.

------
pmoriarty
This makes me wonder if something similar can be done with petri nets.

[https://en.wikipedia.org/wiki/Petri_net](https://en.wikipedia.org/wiki/Petri_net)

~~~
gimboland
Philippe Palanque and the Interactive Critical Systems research group at
Toulouse [1] have been ploughing this furrow for years, on the academic side
but with industrial applications mainly in avionics. So this kinda stuff is
happening, but it's still niche.

See e.g. [2] for more on one of the Petri-net-based formalisms they're using.

[1] [https://www.irit.fr/recherches/ICS/](https://www.irit.fr/recherches/ICS/)

[2]
[https://www.irit.fr/recherches/ICS/softwares/petshop/ico.htm...](https://www.irit.fr/recherches/ICS/softwares/petshop/ico.html)

------
beaker52
This is the most basic multi-state machine you could make. You could do this
with a typescript enum, giving you compile time type checking "for free".

I've tried this "state machine" approach but the problem with designing a
React/UI component to respond to a single "state" value like this is that it's
not uncommon to end up wanting _more_ states like "has errors and loading", or
"has data and loading". At which point you find you're building lots of
similar looking subcomponents which get torn apart every time you add a new
attribute to the combination. Sometimes it's easier to deal with the
combinatorial number of states by passing in the attributes separately and
deal with the combinations of state with if/ternary statements, exactly like
in the original example.

Plus you don't have to have the switch statement argument with someone for
whom not using them was the only thing they remember from Uncle Bob's Clean
Code :grin:

------
andreiursache
Another good example is how in MATLAB an app UI can be integrated with state
diagrams (Stateflow charts)

[https://www.mathworks.com/help/stateflow/ug/analog-
trigger-a...](https://www.mathworks.com/help/stateflow/ug/analog-trigger-app-
using-stateflow.html)

------
NicoJuicy
Something more advanced, but similar to FSM is GOAP.

It's used in calling AI for bruteforcing possible actions according to a state
that a NPC can use.

I recommend to read it/check it out, the AI of FEAR ( which is excellent) is
based on it.

~~~
dkersten
For anyone who wants to try it, I played around with GOAP a little last year.
Here are a few things I learned that might help someone else get started:

Represent your state space as a graph and run A* over it to find a “path” (or
plan) from starting state to goal state. A simple heuristic to use for the A*
distance heuristic (which I saw in one of the gamedev talks, I don’t remember
if in FEAR but its likely it was) is to simply count how many variables are
correct and use that count as the distance heuristic (they used simple boolean
variables, if you use numeric values it may be harder, but I suppose you could
still just count how many goal variables are met).

I also attempted to use prolog-style logic programming (in clojure’s
core.logic) to search for a plan, but I never finished it. I guess I
overcomplicated it by being too ambitious. I wanted the actions to be able to
have preconditions like “have at least N of X” and the goal to have similar
conditions and have the planner keep track of counts. It worked to a degree
but I never finished it because it started getting too complex for my limited
knowledge of logic programming.

------
AstralStorm
The fun part is when you need to integrate anything that's not synchronous
into a classic state machine. It just does not work.

Message passing between FSMs usually does instead and makes them simpler.

------
ahartmetz
IME pure finite state machines are too limited. E.g. on Android, you have a
"go back" stack that you can integrate into a custom state machine, but not
into most state machine frameworks. Another difficulty is animated
transitions. While transitioning, are you in the start state or the end state?
It depends on why you're asking!

If you can make it work, a state machine for navigation is great, but it's
often not so easy.

------
benzoate
Swift is extremely good at this pattern. I’ve been using this pattern a lot
with associated values. You remove any ambiguity in the state of your view
controller, and any unhandled state generates a compiler error (so long as you
doggedly refuse to implement a default handler).

~~~
kolodny
Nice, Swift also handles having all the transition changes as well. I wrote a
small post about how to do this:
[https://gist.github.com/kolodny/6fa6aa34a711d36e9de01cec4409...](https://gist.github.com/kolodny/6fa6aa34a711d36e9de01cec44091557)

------
pcmaffey
> A simpler approach would be to have a single variable for state, and only
> allows switching between well known states!

In web apps, this is the route, as the browser comes out of the box with a
familiar and handy UI for traversing and sharing state.

------
thecleaner
Isnt this state based transition what react/redux tries to accomplish ?

------
shhsshs
This translates well to backend code too. Really _any_ programming.

