Using State Machines with React to Make a Confirmation Dialog (2020)
daveceddia.com
daveceddia.com
No 'finicky useEffect', no race conditions, easily packaged as a component, no dependencies and maybe a couple KB in size. Easy to understand for everyone.
When you have state explosion, it gets complicated really fast as they get bigger. State machines help you manage that by telling you when you've missed something. They scale in a way that a bunch of hooks doesn't.
Right you are.
We should also acknowledge that state machines are a fundamental tool of GUI development. Either you use them, or you use them even though you're oblivious to them. There is no way around them.
Some GUI frameworks like Qt even develop and support their own state machine framework to drive their UIs.
https://doc.qt.io/qt-5/statemachine-api.html
I mean, what do UI design people think a UI flow diagram is supposed to be?
Of all things to complain about React, state machines aren't one of them.
- The number of possible transitions scales quadratically with the number of nodes
- They're not very composable
Plus, there are many things that compile down to state machines, such as Regex or async/await, because they're a pain to write directly.
The number of possible transitions thing isn't really an issue. For embedded applications I usually use a (home grown) Python tool that generates C code from a CSV file. The CSV file is 4 columns - start state, event, end state, action function (optional). That list of transitions can get to hundreds of lines, but grouped by starting state it is fairly easy to manage and understand - e.g. you're looking at the list of things that can happen when the device is 'CHARGING' (such as charger disconnect, power on).
The author explained that it's just a hook to introduce the concept of state machines, but this kind of complexity seems to be cheered on, and you'll easily find code like this, or worse, in production apps.
It is essential for developing complex component, where to accomplish a single transaction requires a series of actions that is inseparable from each other, e.g. creating a viewable configuration object that has internal dependency among itself, or complex annotation tools.
To keep you sanity to handle the dependencies, and failure case, you need a state machine, period, or you invented your own without knowing, or your code is just too broken to be fixed.
With svelte, a state machine is just a variable or two, that you can just assign to. Rendering (and programmatic hooks) react to those variables changing without any effort on your part. And those variables can be local or shared between components, just like any other piece of code.
It’s still, obviously a state machine - but because you can implement it with half as much code, you’ll have half as many bugs and you’ll finish faster. The outcome is the same, but the expression is better in my opinion. (There is something to be said for having all your state in one place - but you can just organise your svelte code like that if you really want to.)
Because I agree with you that Svelte is great and reactive programming is powerful but if you had a problem that looked and acted like a state machine, why you wouldn't just use a state machine? And if someone had a library ready to go that managed it for you, so much the better.
React too can be used without a state library or a global reducer but they are tools you reach for when complexity strikes.
It seems unlikely that svelte’s coding model is a silver bullet that eliminates the need to use more advanced coding strategies to handle the essential complexity of larger state fuel applications.
FWIW this post wasn't written around the idea that modals in React are so hard they require a state machine to get them right, more that I wanted a minimal-ish example to show off how to build something with a state machine, and ideally something real-world-ish, but that wouldn't require special domain knowledge.
It's always a tough balance to strike when choosing examples... too simple and they come off as overengineering, too complex and you lose the forest for the trees. Thanks for checking it out, at any rate!
To me these things are indispensable tools for keeping users happy by preventing me from breaking even more shit.
State machines helped me get rid of the chainsaw juggling with 10 different boolean flags that was responsible for countless bugs and bad customer experience.
It's not over-engineering, it's about keeping things predictable and under control.
I think React has a bad vibe because articles promote adding redux with reselect or other complex systems to login forms or counters, thus normalising overkill.
Also, Svelte is amazing. It provides observables and computed properties which are very productive tools for state management! If you want them in React, `npm install mobx`.
If we're going the very simple here, then you could just use plain old JavaScript...
As well in a full application, Svelte actually has an {#await} syntax that allows you to wait on a promise. I personally would then have a variable representing the currently waiting promise, then use await to show the status/error of the promise.
You want to make the friction for colleagues to use this as low as possible, so that it will be used by default for dangerous actions, without cluttering your business logic!
(Of course, in implementing such a thing, a state machine is a great choice, and OP's article covers much of the complexity you'll need, though possibly using a library vs. a reducer is overkill!)
Both of these libraries' React integration involves creating a "service" that accepts events and transitions between states internally, bypassing React's own state management lifecycle, which is tightly integrated with rendering. In the past I've seen a number of nasty bugs pop up when precise synchronization between theses two sources of truth for state is required, and they can be a bit of a nightmare to debug and/or work around.
I think a better, more robust integration between state machine implementations and view layers like React with their own mechanisms for state management could potentially involve getting rid of the "service" middle man, and instead delegating internal state transitions of the machine at runtime to React itself, using a mechanism like the dispatch function from useReducer, which seems like a perfect fit.
I've been itching to explore this concept a bit more deeply but haven't found the time or motivation between work and various side projects. Would love to see other folks pick up and run with it if they're interested though!
I've been using XState and React on a fairly sophisticated large scale application for the last two years. The key is using React hooks with pure components, and eschewing the lifecycle altogether. This turns React basically into just a highly efficient view rendering library, while delegating all of your application logic to the state machine. Your components end up way cleaner and easier to reason about when they are just pure functions without side effects, and all your business logic is contained within a state machine.
Especially since even if our endgame is to go all-in on the state machine as our only source of truth for state, we could still benefit from a smooth transition path to get there.
Using state machines deliberately for complex flows while delegating simpler state management tasks to simple useState hooks should ideally be just as robust and well supported as modeling our entire UI as a giant state machine.
At the end of the day, the state machine definition in both of these libraries is just a bunch of data, to be interpreted by some runtime that's responsible for maintaining internal state and reacting to events. Currently that runtime is a generic stateful "service" implementation provided by these libraries, but there's no reason why that runtime couldn't be some stateful service implementation built on top of React, or any other view layer with its own opinions on state management.
This is how I approached using XState in a proof of concept last year. I created a “service layer” made up of XState machines that handled different high-level aspects of the application behaviour; i.e. not tied specifically to a particular UI element. Things like managing the requests made during an OAuth flow and storing the authenticated user’s information.
This gave me a source of application state with well defined behaviours that I could use instead of “redux” and its usual accompaniment of boolean flags and thunks.
The best part of embracing the separation of state management from React is that I initially developed the application in Angular before lifting-and-shifting the “service layer” over to React and sticking in a “context”. All the application behaviours remained the same, it was just a matter wiring up some JSX in place of Angular’s HTML templates.
Can someone explain this in a little more detail? Particularly the eschewing the lifecycle altogether. Does this basically mean delaying the view render until all the business logic is evaluated, aiming for only one render per operation?
We use it in conjunction with MobX observers, so that a component is only ever rerendered when its' data is updated, and there's no fooling around with `componentDidUpdate` stuff that can get unwieldy really fast.
The interaction flow is basically: Click handler -> Send event to the state machine -> State machine evaluates business logic, calls services, etc. and updates MobX model -> MobX observer rerenders the pure functional React component. So that all your components are ever doing is sending events to the state machine, with no business logic baked in. Use `useState` for ephemeral component level state like toggle switches, but the source of truth for the entire application lies in MobX models updated by state machines.
You’re right in thinking keeping state management in react is an asset. It’s where the state should always be, in my opinion. React is excellent at managing state and the more ways you can avoid doing it elsewhere, the happier you’ll be.
I’ll see if I can pull some sample code out without violating any contracts. I had a lot of fun putting the hook together - it was the project that totally sold hooks for me. They’re remarkably powerful due to ease of compositions and predictability of their state management. Combined with typescript it has been a total game changer for me.
It does a few things differently from XState / Robot bindings:
1. Use React's useReducer for state transitions and useEffect for applying the effects.
2. Allow reacting to component props (or machine context) and transitioning within the same render where props changed (achieved with a dispatch in the render body). This means the component's children get rendered with props and machine state that is in sync.
3. Separate machine's context (often component props) from machine's state. This reduces the number of unnecessary rerenders, while allowing your machine's reducers and effects to use up to date data and functions passed via props.
All in all, it's my exploration of how to more tightly integrate statecharts into React's render lifecycle and programming model.
Love the fact that you've settled on mimicking Robot's one-way-to-do-things functional composition API (that's my preference as well), and have plans for maintaining compatibility with XState's ecosystem for visualizations (that's imo XState's killer feature over Robot). This really looks like it has the potential to offer the best of both XState and Robot _and_ offer a more seamless React integration!
Will definitely be giving it a shot and keeping a close eye on future developments.
I suspect this example would have some surprising behaviors if the component orchestrating the confirmation flow were rerendered while the confirm dialog is being displayed. Does the state machine get reinitialized, closing the dialog? Or is the ‘confirming’ state retained, but the onConfirm callback in the context updated? Or is the ‘in flight’ state machine retained? In any case, is that represented by a state transition or is the state machine just replaced with a new one?
A state system more tightly connected to react’s own model would allow it to address those concerns more directly.
I wrote this post last summer after getting a chance to use Robot on a project to manage some complex state, and wanted to write an "intro to state machines" in the context of React.
We used it for things like scheduled periodic syncing of data to the server, managing app initialization when data could come from the server or localStorage and could fail (in an offline setting), and a couple other things. It gave us a lot of confidence in the code and I thought it'd be worthwhile to try to get the idea of state machines in front of more people, with a simple (and somewhat contrived for the sake of) example.
From the other comments I gather folks are taking this as a recommendation to use a state machine for modal dialogs, or a suggestion that this is necessary in React. My intent was more to introduce the concept of state machines with a simple example, not so much to say this is the right way to build modal dialogs :D
I think state machines can really help simplify some kinds of problems, and I've been on the lookout for places where they're a good fit. I'd be interested to hear examples where a state machine was useful in code you've worked with.
Okay, so, I have a question about state machines but I’ve been too embarrassed to ask on HN. This is my opportunity, so I’m gonna jump right in.
My question is: why don’t we see state machines _absolutely everywhere_? Or, do we, and I just haven’t appreciated it?
They’ve been introduced in the article as a great new alternative to - presumably - whatever you’ve been doing to manage your (in this case, UI) state up until now. But aren’t state machines self-evidently the best way to manage any arbitrarily complex combination of states and transitions, regardless of the problem domain, language, etc? What are the sane alternatives?
A number of commenters to this thread have said something along the lines of: if you’re not using state machines, you’re using state machines without knowing it. Perhaps that explains the blind spot?
All this being the case, why do they seem to get so few explicit mentions, eg, here on HN or on SO or otherwise?
Love to hear people’s views - please set me straight!
What I mean by this is you can describe virtually every piece of "logic" in an app as a state machine - there are "behaviors" (finite states) that might change (transitions) due to something that happens (events).
The problem is that there are many ways developers wire those up, where the finite states, transitions, and events are there, but it's difficult to enumerate them clearly.
So why doesn't everyone use explicit state machines? Simply because languages make it easier to write implicit state machines (e.g., async/await), and it's the path of least resistance to getting code to "just work". Unfortunately, most developers care about the frictionless path rather than the most robust path, especially in this startup-driven world.
For me, I learned about state machines during my CS degree. They seemed like a neat curiosity, and we drew out our DFA diagrams and whatever, and then I mostly forgot about them. My classes never tied that theoretical knowledge to actual code or real-life examples of when something like a state machine would be useful. If anything, a lot of the examples made them seem like a crappier way to hand-make regular expression, and that made them seem pretty unappealing.
I definitely understand the overengineering complaint, because the code comes out more verbose, takes more thinking to write and read, and forces you to handle all the edge cases that you might normally ignore because "they'll never happen". I still have some resistance to reaching for a state machine, I definitely don't use them for everything.
They've been very useful for a couple recent problems though:
Building a "preset manager" for a screen recorder app. You can have a preset selected, or not. A preset auto-populates the choices for screen, resolution, and mic. If someone changes one of those preset values, or you pull the plug on the mic that's selected, the UI needs to clearly indicate you're not matched with the preset anymore. There were a looooot of fiddly edge cases here and a state machine was a huge help in keeping it tidy, and ensuring that it actually works all the time. I tried to do it without a state machine at first but it got very hard to be sure all the pieces were doing the right thing.
Trial expiration and license management. The app can be in trial, expired, or licensed. Simple enough! But there are other situations, like the license on disk is corrupt, or the trial data is corrupt. The state machine was a big help in making sure I thought through every transition that could happen. Also really nice to have a centralized place for this logic, and the machine can dispatch notifications, so other parts of the app can react as needed.
So it becomes the classic idea of the right tool for the job and knowing when to reach for the more powerful tool. I think that's the value of articles like this.
A dialog should return a promise. Then you can mix confirmation dialog, fetch calls and so on like this:
private async doTheThing(): Promise {
try {
await Confirm("Really do it?); // Promise rejects if user presses No
const options = await OptionsDialog();
await makeFetchCall(options);
await showSuccessDialog();
}
catch (ex) {
showError(ex);
}
}
Yes, that's imperative code, which violates React principles. But it is insanely simple compared to typical React code.In our framework (that has angular underneath) we go..
interactionContext.startActivity(MyConfirmDialog).then(result => ...);
...and the interactionContext handles back button clicks and makes sure the dialog is cancelled and our history is kept clean.It even works with nested interactions (i.e. the dialog can start a new interaction in place). Again, the user can click the back button repeatedly to back out of the interaction and everything just works.
I've got no idea how you'd do that in React.
edit - fixed typos
It's simple, you can place a component that provides dialogs near the root of your component tree and provide async functions which open such dialogs to the descendant components (e.g. via contexts). These functions can then be made to resolve only when the dialogs are closed. Thus, all descendant components can use dialogs in the same manner as shown in your example.
The difference between functional and imperative code is there existing more than one such loop available at the same time, and in how your functions are implemented.
It's the first hook that software engineers learn in React hooks.