Hacker News new | past | comments | ask | show | jobs | submit login
Using State Machines with React to Make a Confirmation Dialog (2020) (daveceddia.com)
90 points by dceddia on Feb 3, 2021 | hide | past | favorite | 64 comments



The over-engineering in React land is really getting long in the tooth for me, and I think will be its downfall. This took under 10 minutes, including creating the modal, and I barely had to think: https://svelte.dev/repl/0299705b5e9e46be9e87fe4fef035bec?ver...

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.


The article wasn't "To handle simple modals, try a state machine." It was "To handle things with a lot of complicated changing state, try a state machine, and here's a simple example."

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.


> It was "To handle things with a lot of complicated changing state, try a state machine, and here's a simple example."

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.


Except state machines aren't that good at handling complexity. This generally boils down to:

- 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.


State machines do compose, but usually in a message passing environment. That might be a message passing programming environment like Erlang, or it might be a message passing system such as a protocol stack. Every layer in your stack be it bluetooth, IP, or whatever is a state machine communicating with messages to the layers above and below (whether or not it is written using an explicit state machine, most protocol layers implicitly are a state machine).

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).


That's why statecharts exist.


Yeah, I think I over-reacted to the introductory line of 'Ever needed to wire up a confirmation dialog in React?' which colored my reading of the article.

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.


State machine via dialogs sounds like a UX anti-pattern.


Right, but your example has nothing to do with a state machine. Nearly the same code could be written in React. I don't share the author's same view of "finicky useEffect" etc. Using a state machine for a modal is pretty overengineered, but I don't think that's reason to switch away from React altogether.


State Machine is just state machine. I don't understand the immediate association you implied with over-engineering.

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.


Sure; but with redux the state machine is site-wide, and needs to be modelled with actions and reducers.

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.)


The transitions between states in the Svelte example are also completely uncontrolled and there is no guaranteed flow from state A => B => C. That is completely fine in many cases, most cases I would say. The problem is when you encounter a scenario where predetermined flow and guaranteed state transitions matter and you're trying to cobble it together. Robot and XState aren't React libraries, they're just Javascript libraries. Not every solution needs them but to dismiss the idea as overengineering or React specific because you can assign to variables in Svelte is missing the point.


Sure; but you can handle that as we do in other programs: if it makes sense to do so, make a single method responsible for modifying a state variable and call that instead of assigning directly. And that can manage side effects. Alternately, trigger other changes off the state variable changing using svelte’s reactive code blocks. This style of programming can scale up and down in complexity like any other code you write, and you can scope it and refactor it along with everything else.


What is your argument here? That we already work with state machines implicitly in UI code so there's no point in making it explicit with a library? That Svelte's reactive mechanics are so good that it doesn't make sense to use an external library even if it is specialized for modelling things as a state machine?

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.


Yes, and at a certain level of complexity you might decide to reify the state transitions in your centralized svelte state into a concrete state machine. Perhaps you will use Robot to do that!

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.


OP here! Thanks for putting together the Svelte demo. I'm a big fan of Svelte too, and I concur... everything I've tried to build with it so far has felt simple and easy compared to React.

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!


I think some people just don’t get the point of state machines, whether they understand them or not. Much like people who think static typing is just cognitive overhead, or things like exhaustive pattern matching just get in the way.

To me these things are indispensable tools for keeping users happy by preventing me from breaking even more shit.


When I worked integrating payment gateways I learned to love state machines. They make code easier to maintain.

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.


We can over-engineer "hello world" in any framework or language :)

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`.



agreed. some workplaces use it as a competitive tool. build an overengineered product to make it look like you're contributing more value than your coworker etc.


React really is best used for larger applications.

If we're going the very simple here, then you could just use plain old JavaScript...


Is it common practice to put main thread blocking async await Fetch requests in UI components in Svelte? That's a very bad idea. The user can't so anything else in the app until that request completes. If they're on a slow mobile connection the UX is going to be pretty horrible, especially as the component doesn't give the user any feedback that something is happening or any option to cancel.


To start off, putting an async function there just means the promise will be executed somewhere, not that the UI thread will stop. Svelte does not wait for promises in events on elements.

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.


What in hells name is an #await doing in the HTML render function?


The #await is a Svelte feature that lets you handle promises, and show different content when the promise is unresolved/loading, resolved, or rejected. Check out the demo in the docs: https://svelte.dev/tutorial/await-blocks


If you're building a tool used in B2B, data curation, or internal tooling uses, I highly recommend creating or finding an interface for confirmation dialogs whose invocation is as simple as `await confirm("...")`. You can even have a hook that lets you memoize and wrap a callback with this, perhaps even making it easy to condition on having unsaved changes. Have it mount in a place that makes sense in your own setup (you can use context, or it can just mount outside your main React mount if you want to cheat), with styling that matches your design system.

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!)


I'm using sweetalert2[0] which is good for success error info messages; warning / question confirmation dialogs; and input / select dialogs. Also got a nice optional timer and the animations can be disabled (we do lol)

[0] https://sweetalert2.github.io/


Self-replying to add that it's important to know when not to use a confirmation dialog (for instance, in a "critical path" of rote data entry), and that your customers may organically show you that things are critical-path that you may not have thought about. In that case, though, having a good abstraction for confirmation dialogs is even more critical, because it's easy to remove (or A/B test) without feeling like you're throwing out significant amounts of effort; it becomes a zero-point story, so to speak. When you're writing less code, there's less code to remove when that becomes necessary, and that's why abstractions are such a big part of our roles!


Thank you, this is really great advice. These kinds of dialogs are always a hassle and are often skipped for that reason.


I'm definitely a fan of the concept of using state machines for UI development, but both of the libraries I've tried so far, XState and Robot (or at least their official React bindings), seem to have a bit of an impedance mismatch when used with React:

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!


>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'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.


I think that's certainly a valid approach, and comes with a ton of benefits. But I don't think that has to be the _only_ viable approach to use a state machine in React without having to worry about subtle synchronization bugs.

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.


”Currently that runtime is a generic stateful "service" implementation provided by these libraries”

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.


> The key is using React hooks with pure components, and eschewing the lifecycle altogether.

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?


>Can someone explain this in a little more detail? Particularly the eschewing the lifecycle altogether.

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.


I can’t share the code, but this is exactly what we do on my team. Using typescript and a light wrapper around useReducer, you can create a fairly robust state machine hook which uses a fairly simple interface to represent various valid states and how they can transition safely.

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.


For what it's worth, I've been working on a state machine library that's more deeply integrated into React: https://github.com/humaans/react-machine.

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.


This looks almost exactly like what I was hoping for! Thank you for sharing!

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.


Agreed.

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 think Elm addresses this issue in a way. It has some sort of "update/render" cycle baked in, with strong typing and sum types.


As far as I understand (though I could be mistaken) the xstate hooks ensure that the component only re-renders when the machine has "settled" after a transition. I don't see how the dispatch function from useReducer is any different. Both useReducer/dispatch and useMachine/send are sending an event that is then handled before triggering a rerender.


IME, what ends up causing issues is the fact that the component calling useMachine can rerender with new state/props that the machine may depend on, causing the machine to sometimes have an outdated view of the world, since its own internal state management (for things like the "state" itself as well as context, guards, other functions) is not fully synchronized with React.


Sure but you have the same problem with useReducer too. If that is a problem then you should be pulling the call to useMachine up to an appropriate level then passing the service down for useService (in xstate) so that changes to the component don't impact the useMachine call.


Author here!

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.


> My intent was more to introduce the concept of state machines with a simple example ...

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!


Here's the funny thing: they are absolutely everywhere! It's just that the vast majority of them are implicit.

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.


I think it's a few things, but the two that seem most apparent are (1) that not that many people know about them, and (2) that they feel like overengineering, too much work, or just too hard when you can just write the code the normal way.

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.


In my opinion, state machines are the best solution when you have "any arbitrarily complex combination of states and transitions, regardless of the problem domain, language" to quote you. However most UI state patterns are not arbitrarily complex. Mostly we're pulling a little data and there is a loading and error state. Using a state machine to manage that is perhaps a little much compared to other solutions and provokes these responses of overengineering.

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.


In Elm/ReasonReact before Hooks it/was the only way to write your application making impossible states unrepresentable


This is a good example of how fucked up React can be when you religiously follow 'UI is a function of state' principle.

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.


React examples like this always strike me as a great reason to avoid react, but then I'm an old OO dinosaur I suspect.

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


>I've got no idea how you'd do that in React.

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.


You can still do this with React, though. It's quite easy (and useful!) to have components which implement some dialog which pops up when you call an async function, which only resolves once the user has finished interacting with that dialog. The code could, for example, be essentially identical to your example. It also isn't in contradiction with the nature of React at all, projects that use React are full of imperative code. React just frees you from having to maintain the connection between UI state and your data manually in an imperative manner.


That’s exactly the API I expose when implementing modals in React. You don’t have to make life complicated.


That's a display, read, react loop. Yes, it's imperative, but this loop is always imperative. Get a FRP GUI toolkit in Haskell and you'll see that loop there, imperative.

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.


What app has a UI that is a series of modal dialogs?


This is what useReducer is for at its core isn’t it? You dispatch named type actions and it executed accordingly. Seems like that would work just as well for this, honestly.


A reducer is pretty close to a state machine. The main differences are that state machines generally prevent invalid transitions and have tooling to help you handle and visualize all the states and transitions.


I think I make state machines with React all the time! Basically what you describe in Redux reducers is state machine transitions. I thought it's the entire point, and common knowledge?


The main difference is that reducers are arbitrary functions (state, action) -> state, without constraining the possible transitions.


But don't they describe the possible transitions? At worst, you can end up with an NFA instead of a DFA.


React has built in hook to handle state in components, useState.

It's the first hook that software engineers learn in React hooks.


I believe the point of the state machine wasn't to save the state but to show how to handle transitions from one state to another using a state machine.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: