Formal Forms with State Machines
Watch on YouTube (ReactNext)
What is this talk about?
“Such as this control, which already has its own local states, so you still have state spread out through your app, it's just a little less organized and less predictable than just having everything be local.”
“One way that we could tackle this complexity is with a mathematical model called finite state machines and statecharts, which is an extended version of finite state machines.”
From the opening of the talk.
Key takeaways
- Such as this control, which already has its own local states, so you still have state spread out through your app, it's just a little less organized and less predictable than just having everything be local. 3:00
- One way that we could tackle this complexity is with a mathematical model called finite state machines and statecharts, which is an extended version of finite state machines. 11:00
- I just want you to have the idea that instead of just returning the next state, we return a combination of the next state and the next effect. 16:00
- You could model finite state machines with switch case, where you consider the current state, such as searching the event that happens, such as resolved and the next state that's going to occur. 20:00
- Instead of having one global state, you have many local states which communicate with each other in an hierarchical way. 30:00
Quoted from the transcript.
Full transcript
From YouTube auto-captions. Punctuation and capitals were restored automatically. Technical terms were normalized; some errors remain. 5,119 words.
0:00 Introduction
Hello, I am so excited to be in Israel and Tel Aviv for the first time, and in the hotel where my name is everywhere. If there's one thing I learned here so far, it's that I do not know how to pronounce my own last name. Because I've had so many people correct me, so that's great. So I'm going to be talking today about forms. Forms are one of the worst parts of web development and application development. Doesn't matter if you're doing native developments or React, or Angular or anything. They're just terrible, and I'm gonna tell you why they're terrible and what we can do about it. So I a few years ago I wrote the foreign library, and I really foreign library, because I thought it was a good idea. At the time I was playing around with real XForm and I, I saw that it didn't have everything that I wanted to do.
Plus, I was fresh to reactor, sort of fresh, and I was coming from Angular version one, which actually had a really nice way of handling forms. You just put the model of what you want to edit and the input or the controller buttons, they handle it automatically. But I soon realized that writing a foreign library is a bad idea. Just ask Kent's Dodds. Just ask anyone who's written in the foreign library before. So, first of all, the name was a bad idea. I wanted it to be similar to Redux form, so I named it React Redux form, which caused a ton of confusion. In fact, a lot of issues for Redux form were in my library, and a lot of issues for React Redux form were in the Redux form repository. So it did cause a lot of confusion between React to Redux form and Redux form.
Plus, look at all those issues. That's a sign of maintainer fatigue. I just did not want to work with it anymore. Using strings like an Angular version one was also a bad idea, especially because, you know, now that I learned TypeScript or other people are using flow, we just know that strings cannot be strongly tight. In fact, using Redux alone was a bad idea, and there's there's many reasons for this in. I don't know if marks in the room, I'm sorry, but so using Redux? For form, state was a bad idea. If you're using Redux, that's totally fine, but for forum state there's better options. And here's why with Redux. Redux has this idea of a global store, a global single atomic store that holds all of your data. Especially states that involves multiple components communicating with each other. Local state, on the other hand, has localized states where each part of the tree has its own local concern.
And in instead communicates via passing props or calling callbacks with parents components. The problem is, when you're using Redux, you're still not fully using Global, this global atomic state, because you might have third-party components. Such as this control, which already has its own local states, so you still have state spread out through your app, it's just a little less organized and less predictable than just having everything be local. So Dan Abramov, of course, he was one of the creators of Redux. I has stated himself that you should use React for ephemeral states, which is state that's just passing, or local, or only relevance to the components at the time of render, and use Redux for state that matters between components or between separate concerns, so state that matters globally. And finally, he said, the rule of thumb is just to do whatever is the least awkward thing to do. In other words, be idiomatic. So thank you Dan, that's great advice.
But handling state in multiple ways was a bad idea, because once you have multiple different ways of handling states, things just get sort of messy. I mean, just ask Israel Ders anyway. Formic so Formic is one of the largest, are not largest in terms of size, but one the most popular foreign libraries for. React right now in cludes a lot of helpful utilities for handling forms, handling inputs, handling whether something is being submitted, handling validation. And it does so in a way that's unobtrusive and, you know, lets you have control over your form for the most part. Reacts final form is another popular library, which is based on final form, which is sort of an iteration from the previous Redux form. It's a sink reader who, you know, surprised he abandon Redux form as well, just like I abandoned React Redux form. And if you look at both of these libraries, you'll see something.
Very similar you'll see that it includes this magic render component or something I think they're gonna start using hooks now but regardless the idea is the same where you take props you pass it into these inputs in you don't care what's in these props that's a good thing and the bad thing number one it's a good thing because you don't have to care you just put it there and things magically work it's a bad thing when you want to control how things work so I recently saw this tweet by Sam ciccone where he describes a what should be a simple form and it's sort of a wizard form so we have three inputs which depend on each other so you could think of it as having a step 1 step 2 step 3 and then each of these steps has asynchronous validation which is already a scary thing and then once you submit the form you have a loading state and then you have either a success state or an error state and if your form submission fails you should be able to retry very simple the requirements been in the tweets but still this is difficult to do even with the existing libraries and so the reason why is because you know for some cases using a form library it's actually a bad idea so if retrieval trivial use cases you could simply use free ask local state and that's fine but once you get into a little bit more complex use cases then you start thinking maybe we should go and reach out for a popular form library such as or make or reacts final form but then once you wants to start escaping whatever those libraries enable you to do and start saying I have specific needs that this library doesn't address then the libraries actually make it harder to accomplish your goals whereas if you just once the approach of not using a form library at all then you know it might be simpler an emphasis on meit's because without properly modeling our forms which is what we're talking about today things can also get messy so either way you know whether you're using a library or not your source grid I'm going to show you why so let's take this async validation example for example we have the value and set value, and we're using state, so just very simple React hooks, who have used using hooks?
Hopefully all of you, yeah, if not, migrate, it's a really good thing to do. But we want some sort of validation state, so we have another use date for validation too. And we want to validate onblur, so we just set it over there and let's see. So we also want to set validation based on async validation, and since we're doing it on blur, we have async validate in the on blur events handler. Not too bad, right? But also, we want to be able to conditionally show or hide whether the whether the input is valid or not, and color it, you know, based on that. So we have this touched or set touched property, we're touched. Means if you actually focused on the input, then only then should it be touched and you should show the validation, etc. And of course, you have to have things were handled, true or false.
And then let's say you want to handle cancellation, I don't know if you could see that, let me zoom in for you, you want to handle cancellation too. So then you have a useEffect and then you have all of that in there, so this gets really, really frustrating and that's only part of the problem. So let's say we have a form and I've run into this issue so many times, and let's say your form has mid button or a fetch data button. The idea is simple you click it, does some sort of asynchronous request and then it finishes. So what if you click and it takes a long time?
9:00 Base size of your app, in which you know the user…
You want to show some sort of loading spinner? Okay, that's not too hard to do, you have this loading flag. But what if you click? And you're like me and you get angry and you think that clicking the button multiple times makes it fast? Or somehow, now you have five API requests and you definitely want to prevents against that. So you're a developer, you're smart. You think I will disable the button and that way they can not click it many times. But if you're also like me, you know that there's such thing as dev tools which you could open. And undisciplined, and then the logic is. The same, so this is known as the bottom-up approach, where we stuff so much logic into our events handlers, and this is where complexity lies. We don't have a centralized place of handling our application logic, it's just spread out through all of our components and all of our events and just random areas of the app.
And so this bottom-up code becomes very difficult to test because you are not entirely sure what's happening in your app. It's also difficult to understand because you cannot communicate clearly to another developer, much less a designer or your manager and the other stakeholders. What's going on in your app? This means that your app will contain bugs. It's a statistical probability. Because there's so many different combinations of things that the user could do to your app in so many different ways. That things could go wrong. In fact, it's difficult to enhance because if you want to add a feature that might touch one area of your app, in fact, you might need to add that to many parts of the app where logic is contained and since your logic is contained everywhere, you're touching many areas of the app. And so features actually make it worse because it increases these states. Base size of your app, in which you know the user could interact with your app in even more ways.
And so there's even more cases where bugs can happen.
11:00 A finite state machine has one initial state…
So one way that we could tackle this complexity is with a mathematical model called finite state machines and statecharts, which is an extended version of finite state machines. Now, some of you might have taken this course in college classes and have been bored out of your mind. It's actually not too hard of a concept. So, finite state machines have five parts, and this might be reviewed to some of you or might be new. So a finite state machine has one initial state, so let's say our initial states. Editing. It has a finite number of states - and this is the important parts this makes it deterministic. It has a finite number of vents, which are the possible things that could happen to the machine. Whether this machine represents a single component or your app, there's only so many things you could do to it. And so we also have a mapping of.
Transitions that, say, when were in the states, such as editing and the submit event, happens now we're in the submitting states. And we also have a finite number of final states, which tells the machine were done. This is our final state, and that's it. So if we take a look at a simple form input, we could see that there's many possible states that this input could be in. It could be disabled, enabled, pristine, dirty, focused, word, valid or invalid, and many things can happen to it. You could change it, focus it, blur it, and if you have custom components, you could do even more things. The main idea that I wants to express to you is that every single component and every single part of your app, whether it's tiny or composed of many components, can be expressed. In terms of these dates and events. So, for example, your input is a state machine, because in order to go from one state to another in events has to happen that causes that transition.
So your, for example, your input might be blurred, and when you have a focus, events occur now your inputs focus. And so this doesn't matter whether you're on the web or on native, it's still the same idea, the implementation details might be different, but the ideas there that you have a focus date, blurred, stayed, changed, a ballad, states, disabled, state, etc. And some of these dates are exclusive, so you can't be focused and blurred at the same time. That doesn't make any sense. So that's the idea of one possible finite state at a time. Also toggles such as checkboxes and radio buttons. There are also state machines, and you could imagine just the different ways that we can navigate between the different states of these checkboxes. Now let's get more advanced. If you're familiar with the Reacts date picker by Airbnb, this is also just a state machine composed of smaller components.
So you see, like, at first, this date picker is closed. And then when we click start state, we're open and we're editing the start date. Once we select the start date, now we're in the state of add editing the end date, and once we click that, then we're closed. So this is how React date picker works, in fact, your entire app. Can be described as a state machine, and you could, you could, you know, edit this at any possible level of abstraction. But you could consider your entire app as a series of states, events and transitions, that transition between those dates. In fact, you could consider yourself a state machine. You know, you're either asleep or you're awake. It took me too long to make this jiff. I can find it anywhere. But so, in order to express this, we're going to use to two hooks, and these are my favorite hooks to use, and these are always in my toolset.
For any React application I use, it's combination of the useReducer and useEffect.
15:00 Our sense to useReducer, which can update the state and give…
So when we're doing things a basic way, where we have an event and whatever happens in the events determines the next state. We know that's incomplete, right? We know that the next state depends on what the previous date was and the events that occurred in that state, but that's also incomplete. Because we need to depend on the state and events and that's going to return the next state and the effects to execute, I, I don't want any of us to think anymore that effects are just something that happens or something that we have to, you know. Consider a last minute concern, but instead consider effects a core part of how your application works. And so the combination of users reducer and useEffects can really help with this events. Our sense to useReducer, which can update the state and give us a combination of next state in effects, we're used to using useReducer to only return the next states. But if we change our thinking a bit and think, what? If we?
Declaratively represents the effects that need to be executed as part of the state return, then the mental model becomes a lot simpler. And so we'll demonstrate this. Let's see we were just using useReducer, and the reducer returns the next state. The problem with this is we want effects to execute, we know we have to execute these effects, but we don't know when they're going to execute. So we don't know what to put in those square brackets, and we don't know what to put inside. useEffect, useEffect is really weird. It's really confusing, and if you're. Confused by it as much as me, I feel your pain. So what we could do differently? And this is just one idea, basically. I just want you to have the idea that instead of just returning the next state, we return a combination of the next state and the next effect. This is not a brand new idea. It's been discussed on Twitter, which doesn't count, but it's also been in reason React, so if you've played around with reason, you might have noticed there's an update with side effects and it has this exact same model.
In fact, this very much resembles the Elm architecture, too. So the idea is that your reducer would take the state, it would be, structure it, and then it would calculate what the next state and effects are. And it would return that. And so we would take that stay in effect, and we would execute the effects in useEffect. This means that the effect is based on states, which is a very important concept. And so one idea of this is like we could update a fetching state and also return. Hey, you should, that's the data to. I'm using an object in this case, but you could also return a function because you know functions can be considered objects or, or you could use a plane object such as this. And so we're returning both the next state and the effects to be executed, or if there are no effects to be executed, we could just return the state.
And so the idea is that we need to realize when. If that's happened and they don't happen randomly, there's only three possible places that, in effects, can happen. It happened whenever a state is entered, whenever a state transitions from one to another, and whenever a state is exited, these are the only three places. And regardless of where the effect occurs or how it occurs, it is part of the next state returned. So that's the main idea. They don't occur randomly, they occur as part of states changes. And so this helps get rid of this whole idea that a lot of us use. Like this empty array to signify effects that should only run once on component did mounts. But it's, it's such a it's like an anti pattern that we've accepted, and you know, it's. Not really the best idea. So instead, we could just base every single state change and every single effect on those state changes, which, you know, it's a better idea.
So let's go back to the original problem. We could model this as a state machine, where we have first, second and third questions. And when we go to the next states, you know, it transitions between those states. And then we when we submit, we'd go to this loading states. And then some promise resolves, and eventually we go to a success. Or era states, and in the era state, we could retry. And so looking at this diagram, which, you know, of course, I had to craft by hand. This diagram makes it really clear what's happening in our application and really clear what the features are, what can be done and how the user could navigate through the application. You could model finite state machines with switch case, where you consider the current state, such as searching the event that happens, such as resolved and the next state that's going to occur. But I like using objects just because it's a more sixteenth way of doing it.
And so with objects, you could just model it the same way where you have searching using object lookup. To figure out what's the next state is based on the events given, and so a simple transition function can take that object and determine. You know, that states, so we get the current state, we get what the next state is based on the events. And if there's no next state or no state defined for that event, then we just return the current state. Because the machine did not handle that state. So EXCI is a library that I wrote about two years ago that does all of this and more. It does state machines, hierarchical state machine, two orthogonal parallel, which I'll go into in a little bit. And the thing is that it takes that object format that we talked about, that you could use without a library, and it just wraps it in the machine. Which gives you a lot of nice functions and methods for dealing with those states, and so you could also interpret these state machines and send it events, and so now you have a running version of your machine.
That acts sort of like an event emitter, so sort of like Redux is subscribe, where you send it events and it returns. Or in the midst, new states that you could subscribe to. So let's take our original example, our async validation loading failure success form. First, we describe the states right over here, so we describe our first data is state one, and all of our states are over here. Then we could fill each of the states with transitions just like that. So we saved like three on next loading, two on next three, etc. And we could use another package that I recently released about a couple months ago called XState React. And this. This introduces a new hook called useMachine, and you could consider useMachine. Just like useReducer. You give it a machine, in this case the wizard machine that we just created. And you get the current state and a send function. Now I like saying send instead, dispatch, just because it's shorter, but it's the same idea.
You're sending events to this reducer, really a reducer on steroids, and it gives you the current state and a whole lot more. And so it also includes methods such as matching based on what the state is, and also a nice, convenient way to send events back to that state machine.
23:00 You ideas that you the more states you have…
So this is what that state looks like. We have the finite state over here, which is the value, we have the context, which is all of the extra data that doesn't necessarily pertain to the finite states. Such as editing, loading, step 1, step 2, step 3, but just that it like whatever the user typed in and actions, which are your declarative array of actions to be executed. So you might be wondering if the scales and it does not necessarily scale once your app reaches a certain level of complexity. Just because you might have multiple states that are redundant, and you might have multiple transitions between those states, and this is known as the state explosion problem. This was a very risky slide to edit at the airports. But yeah, and so you ideas that you the more states you have, the more transitions between those states happen as well. So in this really, computer scientist professor named David Harel actually created this formalism.
1980s called statecharts, which are extended state machines, and these make state machines really, really nice to work with. He was working with the Israeli air force, or something like that on their software for controlling their aircraft. And found that this was a really great way to model the software that controls various parts of the aircraft. So statecharts are an extension on state machines, in that they introduce actions, which are these side effects that we're talking about? And again, they could happen on entry, transition or exit. They also introduced guards, which only allow you to take these transitions if these guards are true. And so that's just right over, like these conditions. And they also introduced context, which allow in extended states to compliments the finite state of the application. So let's take another look at this wizard machine with our async validation. So now we have a bunch of things happening.
We have our context, which is the state of each one of the steps, and we also have actions that assign to that context based on the events given to them. So there's also the idea of hierarchy, which is states within states, and orthogonality, which is parallel states. So I'll try to describe each of these real quick, so hierarchical states. If we're modeling each of the steps and modeling this asynchronous validation over here, our first pass might be we have a validating state and we might have an invalid in editing state. And we notice that there's a lot of redundancy here, where we have submit from both of those and edit goes back to editing anyway. So we could instead combine those two states and reduce the number of transitions. And you could think of these as like events bubbling up the tree, because there's a lot of shared behavior, which is what hierarchical states are all about, and so the way that you would make hierarchical state machines is.
You would just pass the exact same object structure infinitely recursively as you want through each of these states. And then we add all of the actions here, which I'm gonna demonstrate. But, you know, I have less than five minutes left. And there's also the idea of invoking services, where, for example, when I'm in the loading state, I'm invoking a validator, which is a. Promise, and this promise can either resolve or reject, and whether it results or reject, it will do the proper transition defined on the states. And one of the beauties of this is when we leave this loading state, if this promise is still going, it will automatically be canceled. So overall, here's a final example of what this validation machine looks like. So let's say I type something here, it will a synchronously validate, and let's say I accidentally make something invalid. Because I have a number, I could add it again and do that, and then it's loading submitted success.
So you might be thinking, Okay, that's, that's cool, but I could already do this in Exeter, in Redux or Mob X or something like that. So one of my favorite things about using state machines and statecharts is that you could simply copy your machine definition, which you're using in your app. And you could visualize it. So, for example, let's visualize it over here and let's just resolve this. Just there we go. And so now we have a visual way of representing our machine. This is the almost the exact same JavaScript code that were using. And we could see, you know, that we're going through this machine and we could validate that our logic is correct. So if we reject, instead, we should go to the failure state and we should be able to retry, which it immediately goes to the failure state. But eventually it goes to the success state.
And so that's just, you know, one of the best parts about using state machines. So orthogonal states also dress concerns wear many different parts of states might be active at the same time. And so, for example, if you're modeling like a text picker, were yet bold italic underlined, you could simplify that in these orthogonal states, instead of having a combinatorial explosion of states. And so forms and form inputs have this characteristic as well, where we have this idea of pristine, touched, validity, focus, etc.
29:00 Where parents can communicate with children and children can communicate with…
Where all of these might be activated at the same time by same events, and so this represents the combinatorial, the combination of all of these different states. And so again, there's visualization tools where you could just copy and paste any part of your machine or any part of your app which? Is a machine and be able to visualize it? And the reason that this visualization is possible is because it's based on the formalism called SC. XML, which is a very strict definition of what you could do with state machines and statecharts. XState also works with the Redux Dev tools. As you see here, all you have to do is set the EFTS tool to true in your useMachine handler. And X8 4.6 has support for actors now, which I'm not gonna get into, because that's a completely different talk, but the idea of actors is. Instead of having one global state, you have many local states which communicate with each other in an hierarchical way. Where parents can communicate with children and children can communicate with parents as well.
And also I have support for observables now in X8 4.6, which means not only can you invoke observables and listen to events as they come in. But the XState interpreter itself is an observable. And so there's many, many helper packages coming out to XState, including X8 View, which I'm sure none of you care about, that's okay, including this one XState FSM, which doesn't deal with hierarchical or parallel machines, but it instead deals with only finite machines. And it comes as a very tiny bundle size. So there's many examples and use cases which you know you could read about online. If you're using Gatsby, you're already using XState, it's part of the build process in Gatsby, and it's part of many, many other things. Again, there's this SC, XML formalism, first-date charts and lots of resources for you. We have a very active community where we talk about how we could better mul our applications with state machines and statecharts.
So, in shorts, I want you to write code that does more than just do the job, but code that could be visualized, analyzed, tested, simulated, and more. And that's what I hope to bring to you with state machine, statecharts and XState tools. So thank you very much. React next [Applause] [Music].
Related talks
-
Goodbye, useState
BeJS, 2025. 32:02, 15.3K views -
Goodbye, useState
G2i, 2025. 23:58, 50.8K views -
useWat
React Rally 2023. 32:41, 2.7K views -
React Wednesdays: XState and JavaScript State Machines
React Wednesdays (Kendo UI), 2020. 1:09:05