React and visual automata-based programming

How state machines changed the way I think about state management.

Łukasz Makuch
Jan 24, 2018 · 22 min read
Visual automata-based programming on a whiteboard


This post consists of three parts. In the first one I’m telling about my journey to visual automata-based programming. The second one shows the process of building a GUI using visual automata-based programming. The code is written in JavaScript and React is used to render the view. The last part is a summary of my experience with state machines where I explain how did they change the way I think about state management.

The problem I had

There’s something that used to make me struggle a lot every time I was working on a GUI. It’s the fact that views change. The user clicks a button and BAM! Sometimes just a small part of the view changes, sometimes the whole view is replaced with a new one. Moreover, they not only change, but also depend on each other! It’s not anymore like in the good old days, when we had few static HTML files linking to each other. Every time we visited a particular document, it looked the same (I know, I know, maybe some links were purple and not blue, that’s it). Nowadays in web-applications views often depend on events which occurred in the past. Sometimes they display data entered few steps before. Sometimes clicking the same button causes totally different actions depending on what happened earlier.

How was I dealing with it?

There was always some map structure, like a database record, a session object or a plain old JavaScript object that was representing the state of the application. I was then putting conditional statements here and there to compute boolean values based on the state and decide which tiny part of the whole code should actually run.

This approach was good enough, when there were just few screens and they were always changing in the same order, like from A to B and then to C.

When things were getting out of control

Sadly, every time when the number of views was increasing or there was more than just one path the user could take, I was loosing control of all the IFs. When implementing a new feature, I was constantly asking myself:

Answering this question wasn’t so easy, especially when I was working on server-side applications, where forgetting to limit access to some view can potentially lead to insecure direct object references. My development process felt like a dirty hack. In the end what I was doing was using IFs to tell which part of the code should run and which shouldn’t.

Moreover, it wasn’t just me who was asking questions about the system. Except the case when I was working on my personal projects, there were always some people I was working for. They were also asking questions like the following two.

Very often, even though I had the project opened in the editor, I couldn’t quickly answer those questions. In order to reach the IFs responsible for the behavior, I needed to dig deep into the code. It was giving me a headache.

From sketching to the discovery of automata-based programming

I became a big fan of sketching. Nothing fancy, just some pictures representing the overall behavior of the code I was working on. Like here’s a screen like this, something happens and we get that different screen. Very much like user flows we get from UX experts or… one of the fundamental concepts in computer science — state machines. This is the time when I started wondering if I could make use of state machines in my day to day job. I knew that they could be useful if I were about to do things like parsing a language or drawing a representation of a turnstile. But could they help me to program web applications?

There’s a wide term called automata-based programming. The Wikipedia definition of automata-based programming goes like this.

The idea of making a set of boxes connected with arrows the skeleton of an application always sounds great to me! It’s like bringing my sketches to life, or at least making the gap between the language used to program and the language used to design a lot smaller. So I started with a data structure representing the graph and possible transitions. It was very much like transition tables.

Soon it turned out that a simple state machine function, which takes the current state and an event and returns a new state, wasn’t sufficient, because I still needed to use some conditional statements to determine what to do based on its result. So the very next thing I did was using this graph description as a recipe for a stateful object, which responsibility was to call functions based on the current node of the graph.

Since then I experimented a lot and changed many details, but this is one of the things I haven’t changed. I really wanted to run away from IFs and method dispatch based on the state of a state machine finally allowed me to do it. It felt like a relief! I got hooked on automata-based programming.

I missed drawing

It became clear to me that many of the problems I used to consider hard to code can be easily solved using elegant graphs. I do really enjoy expressing logic as graphs! But I must say that when it comes to transition tables, I like them a lot less. To me, it’s harder to wrap my head around a transition table. When I take a look at simple graph, I can immediately get basic understanding of what’s going on. But when I look at a transition table, it takes me a while. Most of the time I try to imagine a graph in my head. When it’s too complicated, I grab a pen and draw the graph, so I can look at it.

I think you already know what I am going to say. Programming by coding graphs is fun, but not that much fun as programming by drawing graphs! That’s why I decided to build a graphical editor that would generate the graph code based on a drawing. This way I could code just what I find handy to code and draw the rest. And that’s how I got into visual automata-based programming.

This is how I was programming a simple graph, when I started playing with the idea of automata-based programming in 2016.

And this is how I prefer to program it nowadays.

The same graph as above, just drawn.

In this post I’d like to show you the whole process of building a GUI, which illustrates all the things I like about state machines applied to web applications.

What we’re going to build

The example application

We’re going to build a client-side application with few screens. At the home screen the user is asked whether she is a bunny owner. The next step is always about entering the user name. Then, bunny owners need to feed their bunnies before they can see the thank you message. Those who have no bunnies see thank you right after entering their names. It’s important to note that the bunny is hungry every time we see it and that we need to first find a carrot before we can give it to the bunny.

Here are few whiteboard drawings showing how screens change.

A whiteboard drawing of the most important screens
A whiteboard drawing of the bunny itself

The toolbox

The two most important libraries used to implement the application are:

- React to render the UI;

- Rosmaro to model changes of the UI.

For testing purposes we’re going to use the following tools:

- Enzyme (to make assertions easier);

- Jest (it allows to run our tests and ships with useful matchers);

- Sinon (to make sure the UI makes proper function calls).

All the packages are going to be tled using npm and the project is going to be bootstrapped with Create React App.

React is going to be used mostly as a Virtual DOM library and a convenient starter kit thanks to Create React App, while both the data-related state and behavior-related state are going to be managed using Rosmaro.

Setting up the environment

Let’s fire up the terminal and use Create React App to bootstrap our project.

create-react-app bunny-appcd bunny-app

Please bear in mind that in order for this command to work, you’ll need to have node, npm and create-react-app installed.

The generated index.js file looks like this.

Now we’re going to install packages related to Rosmaro.

npm install --save rosmaro rosmaro-in-memory-storage rosmaro-process-wide-lock rosmaro-react

Then, the App element is going to be replaced with the RosmaroReact element. Because we won’t need App.css, App.js, App.test.js, we can safely remove those files together with their imports.

The index.js file should now look like this.

Let’s go step by step through the modified file.

Rosmaro-related packages are imported.

The rosmaro package itself is not being imported, because it’s already a peer dependency of rosmaro-react

We’ve replaced the App element with the RosmaroReact element.

For better understanding of rosmaroOpts please take a look at the Building a model chapter of the Rosmaro documentation.

As you may have noticed, there are two extra imports.

They are the two main ingredients of a Rosmaro model we haven’t created yet:

  • a graph describing changes of behavior
  • pieces of code handling different behaviors

Let’s start by creating empty files:

mkdir src/handlers && touch src/handlers/all.js && touch src/graph.json

At this point only test libraries are missing.

npm install --save sinon enzyme enzyme-adapter-react-16 react-test-renderer

According to the documentation of Enzyme this is how we setup the tests.

That’s pretty much it! The environment is ready.

This command, provided by Create React App, starts a dev server.

npm run start

And this is how we run the tests

npm run test

Modeling changes of behavior using the Rosmaro Editor

It’s time to fire up the Rosmaro editor. Because our graph.json file is empty, there’s nothing to import. We can simply click LOAD to start drawing a new graph.

Starting a new Rosmaro project

Every Rosmaro model must have a main node. In our case it’s the following graph.

A whiteboard drawing of the most important screens

We add it by clicking ADD NODE and typing its name.

Typing the name of the main node

Now we’re going to add the first graph child.

Adding a new child

In order to make binding handlers convenient, we’re going to use CamelCase node names. That’s why we’re adding a local node called IsABunnyOwner.

Adding the IsABunnyOwner local node

Local nodes are what we draw as children of composites and graphs. Handlers cannot be associated with local nodes.

Actual nodes are used as underlaying nodes for local nodes. They may be associated with handlers.

In the whiteboard drawing there are two nodes called ENTERING THE NAME. Because Rosmaro local nodes must have unique names, we’re simply going to call them EnteringTheName #1 and EnteringTheName #2.

Added both EnteringTheName nodes

Then, we add the remaining local nodes: FeedingTheBunny and Thanks in the same way.

Now it’s time to add arrows. In order to do it, we need to put the cursor over some node, aim the square handler displayed on it and drag a line from it to another node.

At this point, the newly created arrow can be customized. All we need to do is to click it and then enter its new name. This one is going to be called has a bunny and will tell us what happens when the user has a bunny — she goes from the home screen to the screen where she’s supposed to enter her name.

Giving a name to the edge from the IsABunnyOwner node

Let’s draw all the remaining arrows and call them after events which cause behavior changes.

All main edges are drawn

This graph isn’t finished yet. The whiteboard drawing wasn’t perfectly detailed. There are at least two missing elements:

  • the initial node
  • underlaying nodes

We need to specify that this graph starts from the IsABunnyOwner node. All we need to do is to draw an arrow from the start entry point to the IsABunnyOwner node.

The main graph got an initial node

Selecting underlaying nodes means specifying which graph node actually lays under a local graph node. For example, there’s the IsABunnyOwner local node. But what’s its type? Is this a leaf? Or maybe it’s a subgraph? In order to make it clear, we need to add an actual node and wire it with the local node. It may sound a bit complicated, but it’s actually pretty simple. Here’s how we do it.

First, we need to create the actual node. We do it exactly in the same way the main node was created, because main is an actual node as well.

Adding a new actual node — IsABunnyOwner

Then, we go back to the main node, select the IsABunnyOwner local node and select the IsABunnyOwner actual node as its underlaying node. That’s it!

The Thanks node is a leaf as well. We add and wire it in the same way as we did it with the IsABunnyOwner node.

Thanks added and wired

Nodes EnteringTheName #1 and EnteringTheName #2 are a bit more interesting, because even though they are two separated local nodes from the point of view of the main node, they utilize the exact same actual node as their underlaying nodes. That’s why we add just one actual node EnteringTheName.

Adding EnteringTheName

Then we pick it as the underlaying node for both EnteringTheName #1 and EnteringTheName #2.

The EnteringTheName #1 local node uses the EnteringTheName actual node as its underlaying node
The EnteringTheName #2 local node uses the EnteringTheName actual node as its underlaying node

FeedingTheBunny is going to be far more complicated. Here’s how we decided that it has two regions:

  • the bunny itself
  • controls for carrots (looking for carrots and giving them to the bunny)
A whiteboard drawing of FeedingTheBunny

It makes it a composite with two children.

Adding the FeedingTheBunny composite
The FeedingTheBunny composite with its children

Let’s start from the most important part — the bunny. It can be:

  • hungry
  • eating
  • full

Here’s how does TheBunny look.

TheBunny graph

We cannot forget about underlaying nodes. There are three of them:

  • AHungryBunny
  • AnEatingBunny
  • AFullBunny

They are simple enough to be leaves. We add them in the same way how we added other leaves.

This is how CarrotControls look in the editor.

CarrotControls graph

Both LookingForACarrot and GivingTheCarrot are leaves. They must be added and have their underlaying nodes selected.

At this point, all nodes and arrows are drawn. We’re one click away from generating the src/graph.json file! All we need to do is to click the GENERATE CODE button.

Generated code

The generated code should be saved as src/graph.json.

If for some reason you get an error saying that the code cannot be generated, please make sure that all local nodes have their underlaying nodes selected.

Coding different behaviors

After having some (I hope) good time drawing, it’s time to code a bit. We’re going to start from the handler of the IsABunnyOwner node. The first step is to create a handler file, which in this case is called IsABunnyOwner.js and it’s located in the handlers directory.

A quick look at its render method reveals that it gives the user a choice by displaying two buttons. Let’s make sure it actually renders without crashing. We will need a test for this handler. All we need to do is to create a file called IsABunnyOwner.test.js which is located in the same directory.

Now, when we run the following command, we can make sure that it actually renders two buttons.

npm test

The graph tells us that we expect the IsABunnyOwner node to follow either has no bunny or has a bunny arrow. Here’s the complete handler code.

The render method takes a reference to the model object. It can be used to build event handlers which call methods of the Rosmaro model. In this case, the same handler that renders the UI, also handles method calls. That’s why there are functions hasABunny and hasNoBunny. All they need to do is to return the arrow we want the model to follow when they’re called. If you want, you can find more about this in the documentation chapter about method handlers.

And here is the test case. Please notice how Sinon spy objects are used to make sure that clicking a button actually triggers a model method.

When it comes to the home screen of our application, the only thing left to do is to wire the handler with the graph node. At the beginning of this article we created the src/handlers/all.js file. All it’s responsible for is to provide a map connecting graph nodes with their handlers. This is how does it look with the first handler.

And that’s how we finished the home screen of our app.

The node meant to enter the name, that is EnteringTheName, behaves slightly different. It not only follows arrows, but also alters the context of the Rosmaro model.

You can think of context as of something similar to state from React or store from Redux.

In order to set the default value of the user name, we’re going to wire a simple handler to the main node.

The list of all handlers must be updated.

Since now, every time a child of the main node reads before the context is set to any non-empty value, it’s going to read Unknown person.

The EnteringTheName handler renders a text field and a button.

Please note how we read the name property of the context. The method is called with an object. It has a property called ctx, which is the context. We read it using object destructing.

We want to make sure the view renders without crashing and that it sets the value of the text field to the name read from the context.

Adding the behavior responsible for handling the Done button is very similar to what we did in order to handle buttons rendered by the IsABunnyOwner node.

To sum it up, in order to handle a button click that makes the model follow an arrow, we need to:

  • register an event listener that calls a model method
  • create a model method that follows an arrow

The thing that makes the EnteringTheName node more interesting than other nodes with buttons is that it handles typing on the keyboard as well. We want to let the user type her name, what means altering the name context parameter. The only way to update the context is by doing a transition. That’s why we’re going to need an extra arrow — to handle a situation when the name is being typed but it’s not ready yet so we don’t want to change the screen.

Loops called “typed” for “EnteringTheName” nodes

Now we can register an event listener that will call a model method meant to do a transition and set the new value of the name.

The most common mistake I make writing handlers like this is to replace the whole context instead of updating just one field. Let’s take a look at its test case.

If you were wondering what’s the point of the {another: 123} part of the context, this is the answer — to make sure it’s not accidentally removed. This handler is meant to update the name property. It shouldn’t touch other properties.

There’s one more thing about the EnteringTheName node I’d like to mention about. Let’s have a look at the main graph one more time.

A whiteboard drawing of the most important screens

As we can see, when the name is entered, the user lands on either the FeedingTheBunny screen or the Thanks screen. What’s cool is that the code responsible for handling the Done button always does the same — it makes the model follow an arrow called done. There is no IF for that. It’s not parametrized using a boolean flag neither. It’s all about where is the arrow pointing at.

We’re getting closer to the most complicated part which is the FeedingTheBunny node and its children. The node itself is a composite of two nodes: TheBunny and CarrotControls. Both of those nodes handle the render method in their own ways. In order to make the composite render correctly, we need to make its render method return a merged result of the _render_ method of TheBunny and the render method of CarrotControls. This is how we can use altering results to achieve this.

This is its test case.

We’re going to start from the CarrotControls graph. What makes it special is that it calls model methods which are handled by two handlers:

  • the controls themselves (when we give a carrot away, we don’t have it anymore)
  • the bunny (when the bunny is given a carrot, it’s not hungry anymore)

First, let’s do the controls.

These buttons simply change between Look for a carrot and Give the carrot. They do it by calling model methods like giveTheCarrot. And I must tell you a secret — TheBunny is especially interested in getting a carrot.

To make TheBunny children a bit simpler, we’re going to give them just a slice of the whole context. The whole context looks like this.

But we want TheBunny and its children to see just this.

It turns out to be very easy with context slices. All we need to do is to write a simple handler for TheBunny.

Now we can create AHungryBunny. It will handle the same method call which makes the controls change from GiveTheCarrot to LookingForACarrot, that is giveTheCarrot.

Every time we call the giveTheCarrot method, the node called GivingTheCarrot follows the arrow called gave the carrot and the node called AHungryBunny follows the arrow called ate a carrot (and sets the number of ate carrots to 1).

When AHungryBunny eats a carrot it becomes AnEatingBunny.

Every time it’s given a carrot, it increments the number of ate carrots by 1 (what’s actually done by returning a new version of the context, very much like in Redux). What makes it different than any other handler we implemented is that it picks the arrow to follow based upon the context of the model. If the bunny has eaten at least 5 carrots, it follows the ate 5 carrots arrow. However, if the bunny ate less than 5 carrots, it just updates the number of ate carrots. It accomplishes this by following a different arrow called ate a carrot. It is very much like the EnteringTheName node which follows the typed arrow in order to update the context. That’s why we need to add this arrow to the graph.

TheBunny and “ate a carrot” arrow

Finally, AFullBunny is happy and allows us to go back to what we were doing.

The last screen is called Thanks and it simply says thank you and lets us to start the whole process one more time.

The application is almost ready. The missing functionality is about TheBunny being hungry every time we see it, while the state of CarrotControls should never change without user interaction. It means that if we leave the bunny full and we have a carrot (let’s imagine we’re carrying it in a bag), then the bunny is hungry when we meet it again but we don’t need to look for another carrot because we already have one (the one we found previously).

Implementing this feature is actually quite easy and can be done just by editing the graph. First, let’s use a special entry point to FeedingTheBunny. We’re going to call it feeding. All we need to do is to open the main graph, click the fed the bunny arrow and change its entry point from start to feeding.

Renaming the entry point to “feeding”

Since now, every subgraph of the FeedingTheBunny must have an entry point called feeding. So let’s start with TheBunny, which is always hungry when it comes to feeding it.

  1. Open TheBunny
  3. Type feeding as its name
  4. Click ADD
  5. Draw an arrow from the newly created feeding entry point to AHungryBunny
“feeding” entry point to TheBunny

In order to make the CarrotControls remember the recently active node, we’re going to make use of the special recent node. It symbolizes the node which was active before the graph has been left, or if it has never been active, then it’s the node the arrow from the start entry point is pointing at. Long story short — we need to add the feeding entry point and connect it with the special recent node.

“feeding” entry point to CarrotControls

And that’s pretty much it! The application works as expected and consists of just one IF statement. What’s worth noticing, this IF statement is not mandatory. It could be totally removed, what would make the codebase free of any IF statements, if the whole memory of the Rosmaro model was expressed using graph nodes. However, it would lead to a phenomenon called state explosion. This single IF statement turns out to be surprisingly helpful! It’s one of the the tools which allow us to mitigate the risk of ending up with an enormous number of nodes and arrows, together with:

  • composites (orthogonal regions)
  • subgraphs (state machines in state machines)
  • context (a piece of data usually in the form of a map structure)

Depending on our needs, we can balance the number of nodes and IFs. There may be just one node and many IFs, or many nodes and no IFs at all.

The full source code of the application we’ve just built is available on GitHub — Rosmaro-React-example-Bunny-App.

Modifications worth trying out

Here are few modifications which I think are worth trying out. It doesn’t mean that they represent a better approach. They’re simply interesting exercises:

  1. removing all IF statements from method handlers (hint: it will increase the number of graph nodes)
  2. making the typed arrow disappear from the main graph (hint: a new subgraph will be necessary)
  3. adding a back button from Thanks (hint: depending on the destination screen, new nodes may be necessary)

Does it scale?

I often hear, that finite state-machines don’t scale. I totally agree on that, when it comes to simple, pure FSM-based solutions. When the graph is the only memory and when every single event needs to be explicitly represented by its own arrow, even trivial tasks may lead to an enormous number of nodes and arrows.

A great example to illustrate this problem is a Hello YourName! application, which has one screen with a text input and a button and another screen to display greetings. Even if we limit the length of the name to 2 characters and allow only two letters: a and b, the graph will still look like this.

An example of the phenomenon called state explosion

This doesn’t look good.

However, there’s no reason why we need to limit ourselves to just one-level graphs or even graphs at all. Thanks to the fact that Rosmaro is not a pure FSM-based solution, we can use some of the techniques described above to make the graph look exactly how we want it to look. We could even reduce the number of nodes to exactly one by replacing other nodes with IFs. Of course, it wouldn’t make to much sense to use Rosmaro if we needed just one node. It simply illustrates the possibility of balancing between nodes and IFs in order to achieve a structure that represents the intended behavior of the application in a clear, documentation-like manner.

Here’s the Hello YourName! application modeled using method handlers, context and subgraphs.

The main graph of the Hello YourName! application
The subgraph of the Hello YourName! application

It doesn’t look so scary, does it?

And when it comes to a bit more complex examples, here’s an application with 13 different screens which depend on each other and many paths the user may take.

A more complex example

In the code, there are 3 IF statements. The graph consists of 29 nodes.

The main node opened in the editor looks like this.

The main node of the complex example


To me, visual automata-based programming turned out to be a really great discovery. For a long time, I considered state machines something almost purely theoretical. Or at least not directly applicable to the field I was working on.

But then I imagined the code structured like there would be islands of clearly defined behavior. These islands are handlers from Rosmaro. There are also bridges connecting these islands. They are arrows representing events. The current behavior of the system is changed by going from one island to another. It all clicked. Now I can design and implement changes of behavior in an elegant, organized way, where previously I would just scatter IF statements.

I can also see how I benefited from automata-based programming in two different ways. It’s not only easier to program by drawing boxes and arrows instead of inspecting boolean flags. It also makes it necessary to discover and name all the important states of the application, what’s a great framework for thoughts.

And when it comes to application state, it showed me that there’s state management beyond map structures (which are key-value pairs, like method-less JavaScript objects). A new tool appeared in my toolbox. It’s the state machine based dispatch mechanism. Instead of just accepting the fact that I have some boolean values to check using IFs, I can consider replacing them with nodes. After playing a bit with this approach, I started noticing the difference between data-related state and behavior-related state. Data-related state is something like information about a user. It may be as simple as name and surname. Most of the time, we don’t need to use IFs to deal with state like this. It’s simply there. We can display or save it without conditional statements. A set of key-value pairs is a perfect model for state like this. On the other hand, behavior-related state determines how should the application work according to events which occurred in the past. Sometimes key-value pairs topped with a few IFs do the job very well, other times state machines feel a lot more natural and easy to understand.

If you haven’t tried yet, I hope you’ll give state machines a go. Maybe you’ll also find them valuable in your day to day job?


JavaScript news and opinion.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store