New: Use Finite State Machines
New Pattern Proposal: Use Finite State Machines
Some things - especially components - need to represent state that has no business anywhere outside of the component. We should begin representing this contained state with finite state machines.
Advantages of new pattern
- Extremely testable - if code's behavior is fully encapsulated in a finite state machine, we can test that the state updates correctly at every step of any process
- Extremely readable - instead of fiddling with a bunch of state booleans, actions that change the state of some code are documented as transition events, like
LOAD_SUCCESS
. You don't need to know which flags would have been updated there to know what happened. - Extremely declarative - Instead of inferring what state something is in at any moment, there's a single definition of what state is possible - and how to get there and move out of it.
- State is a single string; it either
is
something, or it is not. No more checking combinations of flags.
- State is a single string; it either
Disadvantages of new pattern
I'm honestly having trouble thinking of a negative, maybe...
- There's a bit of extra code - the machine definition is a moderate-sized object
- States and Transitions should be extracted to constants, so there will be a bunch of extra constants laying around in modules
- It could be confusing to have
state
(e.g. global state, Vuex) andstate machine
(e.g. internal state).- I must note that these are - obviously - different things. I think most people agree you can and should have both.
- Some folks might prefer to deal with clusters of booleans
- Maybe
if( !isLoading && hasError )
is preferable - to some - toif( is( 'errored' ) )
- Maybe
- It's an abstraction of something you could do in a few lines of code directly in a component
What is the impact on our existing codebase?
None at all. This is entirely opt-in, and nothing that currently exists has to be touched.
Adding the reference implementation as is will work without modification.
It is 49 lines of code (plus 329 lines of tests).
It's also eminently reversible: since this is entirely self-contained to the (component|module|class) where it's used, you can add or remove it at whim.
Reference implementation
Boy howdy do I have a reference implementation for you!
gitlab-org/gitlab!32365 (merged)
Prior Art
- "state of the art" @xstate/fsm
- State machine with additional exit context (properties that are returned after a state finishes transitioning, but as part of the definition of the source state) via @ealcantara: https://gitlab.com/gitlab-org/gitlab/blob/v13.0.0-rc20200506140558-ee/app/assets/javascripts/clusters/services/application_state_machine.js
- I added a super primitive state machine in a current MR: https://gitlab.com/gitlab-org/gitlab/-/blob/e0483121c4ce89d0c3890d3bcb20bd1c6ef53329/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue#L50-64
- Backend already uses state_machines for some things.