Why we need a Different Paradigm for Architecting UI Apps
By: Ravi Kiran
Tags: Redux, Statecharts, Xstate

A lot can happen between user clicking save and the network call getting resolved. Let us now freeze this moment and have a conversation.

What happens if the user clicks on the save again? It triggers another save call. Do we want that? No. We know from experience that when a submit button is clicked, we should immediately disable it to prevent users from accidentally or purposefully clicking it again. How do we achieve this? by setting an isSaving flag to true in the redux object just as you make the API call. This flag can then be subscribed by the save button and disable itself.

Okay, one problem solved, a hundred waiting…

What if the user clicks on other form actions like draft or undo or reset or even edit the form? should we allow him? Maybe not. So let's plant an isSaving flag on each of these buttons to disable them.

Next, can the user go to the next record when the save is happening? Not so sure. We need to assess the risks involved in this. Prima facie it doesn’t look safe to allow. Can he go back to the previous record? Again, not sure. Can he go back to the home page? Yes, but what if the save resolves after you are on the home page, will it affect the UI in any way? we need to think through the implications.

This can quickly get overwhelming. We are in the middle of the development and we have so many unknowns. Now, a brilliant idea comes to mind. What if we lock down the whole UI during this hazy period? A lot of uncertainty can be avoided. An overlay spinner to the rescue. Toggle it on while the form is saving and the user won’t be able to perform any action during this period. This can even help us in not spraying the isSaving flag all over the app as well. Great! But there is collateral damage associated with it — the user can’t open the chat and other features that we know have no relevance to saving. Yes, not efficient but we’ll take this to the chaos and the uncertainty that can ensue if left open.

So we successfully deliver the feature. Nobody has any complaints. A week down the line the business comes up with a new requirement: Can we have a keyboard shortcut key to the next and back buttons to make it easy for the users to navigate? By all means.

Add a listener to the keypress event, and map next and back actions to left and right arrow keys. We don’t even need to touch any existing code, so we are all confident about it. The feature is now in production.

A few days later a bug gets reported: User is seeing previous records confirmation message even after navigating to a new record. You quickly check the code to see whether we are clearing the confirmation key from the store object before moving to the next record. We are, otherwise, it would have failed in the first try itself. So how did this happen?

After breaking our heads for a bit, we realise that our brilliant idea to lock down the screen has fallen flat since the keyboard is still free for the user to use. In this case, the user would have pressed the right arrow even as the save was still in process. So? So it is entirely possible that fetching the next record would have resolved before the save, and it created a mess in the UI.

Even though it may be a rare occurrence, we can never rely on the order of resolution of network requests, more so in today’s distributed world. Also, think — what if we are pre-fetching the next record? We will always find ourselves in this case.

So how do we solve this bug? Go back to spraying isSaving flag on all the buttons? We just saw that to disable UI elements means nothing, so we also have to spray the isSaving flag in all our reducers where actions are really processed. Now think about this, all this while we have been dealing with just one simple network call, but there could be more parallel calls, interactions, and whatnot. Now we have to have so many conditional statements everywhere in our app that keep growing without a way to track them.

I know there are complementary libraries for Redux like Redux Saga which lets us deal with some of these problems, especially in canceling tasks, composing flows. But the issue is that it is mostly an afterthought and not something we consciously bear in mind before we start the development. We will also see in the course of the discussion how our current solutions are imperative and why we need an explicit/declarative method.

The Reason

The problems associated with UI code stem from the way the software is constructed. Almost without exception, UI software is coded without an overall software design. The lack of design for UI software results in code with an intricate structure that is difficult to understand and enhance — Ian Horrocks

Again, something said in the last century, holds true even today. So why don’t we design our UI software? I’m not talking about the look and feel design, but a software construction design. For example, we don’t create database tables just like that, do we? Instead, we do a thorough analysis of the requirements and come up with a proper DB design, which can be visually represented in ER diagrams. So that everyone from the database team to developers to business analysts can understand and refer to it at any stage of the project. This helps everyone in determining what is possible and what is not when the requirements change. So why don’t we do this in constructing UI software? Don’t we have tools or notations to represent our UIs?

Maybe we should ask an even more fundamental question — do we even know the nature of the system we are working with to know whether there are tools to model it?

In mid-1980s, a computer professor named David Harel published an important paper. In that he makes a distinction between two types of systems:

Transformational

Reactive

A Transformational system, as the name suggests, is a function that transforms input data of one type into output data of another. For example, you give some data to a system, and it outputs what the weather could be like tomorrow.

In contrast, Harel defines a Reactive system as largely event-driven, continuously having to react to external and internal stimuli. This is exactly what our User Interfaces do. He then goes on to say,

A typical reactive system is not particularly data intensive or calculation-intensive. So what is the problem with such systems? In a nutshell, it is the need to provide a clear yet precise description of what the system does, or should do. Specifying its behavior is the real issue.

We will come back to the central theme of his paper in the next part, but for now, let’s dwell on the above quote and compare it with the current architecture we follow.

The key phrase here is: ‘precise description of behavior’. It means to specify in unambiguous terms what the application can do and what it cannot do at any given point. Now let us see how we are accommodating ‘behavior’ in our Redux Architecture.

If we go back to our example when the save is in progress, how are we basing what can happen and what cannot happen in that state? We are controlling that by turning on a boolean flag (isSaving) and a bunch of conditional statements. Is it the right approach? To determine that let us ask this question:

Can we tell by looking at the object store what events are possible in a particular state and what transitions it can take from there?

The answer is NO. This piece of information is spread across the app; a bit in our store, a bit in our components, and a bit in our middleware/reducers. So the behavior is implicit in the app but not described explicitly/precisely (Imperative vs Declarative)

Now, check out the below visual.

I stole it from this excellent article written by a guy named Lukasz Makuch. I highly recommend reading his blog. What Lukasz is trying to do is bring our attention to an important detail that we are missing: the distinction between Data and Behavior

Let us say we are fetching a person’s record. His data {name: "John", age:22, occupation:"Engineer"} can be easily represented in key-value pairs. But it becomes very cumbersome to fit behavioural details like, what happens when we are fetching a record, what events it reacts to in that state and what transitions it can take from there, in terms of pure key-value pairs. As Lukasz observes, behavior is a directed graph — you go from one node to another: idle to fetching, fetching to fetched/failed — so it needs a different approach. Trying to fit this in key-value pairs results in too many boolean flags to turn on/off and respective conditional statements everywhere in the app.

So the reasons for our problems can be summed up in two points

That we are not designing our UI software

And that the architectural pattern we are currently following is not conducive to the nature of the system we are dealing with.

The Solution

To make UI software easier to understand, a design notation is required that provides a clear view of how the software works together as a whole. — Ian Horrocks

A little history…

Let us go back to David Harel’s paper. Even though transformational systems can be complex, what Harel felt was that there were enough methods to represent them diagrammatically. On the other hand, he thought reactive systems lacked a formal, visual way of denoting their behaviour. He had discovered this shortcoming while consulting for developing an avionics system. So he had formulated a method and later published it in his paper (which is of our interest): Statecharts: A Visual Formalism for Complex Systems

Statecharts concept is not totally a new one, it is just an extension of a much familiar concept called Finite State Machine(FSM). FSM with its State transition diagram is quite a common modeling tool both in the electronics and software world. Though Harel quickly recognized that FSM was the closest existing tool to denote the behavior of complex reactive systems, he also realised its limitation.

“….simply list all possible states and specify all the transitions between them is unstructured and non-intuitive; it has no means for modularity, hiding of information, clustering, and separation of concerns, and was not going to work for the kind of complex behavior…”

So he introduced concepts of hierarchy, orthogonality, broadcasting messages to FSM, and added a few more rules to come up with a complete package called Statecharts for modeling Reactive Systems. He demonstrated it in his paper by designing a digital watch.

Statecharts & UI: In the late 90s, another computer professor named Ian Horrocks, whose name I’ve been quoting all this while, took these concepts and applied to UI software construction. In 1999, he published an excellent book called Constructing the User Interface with Statecharts, which covers solutions to different UI problems using statecharts.

In 2015, after almost a decade long work, W3C standardized Statecharts. (State Chart XML (SCXML): State Machine Notation for Control Abstraction)

Using the ideas presented in Ian Horrocks’ book, and SCXML specification, a brilliant guy named David Khourshid started developing a stellar JS library called XState.

My association with statechart modeling started with the discovery of this library. What blew my mind when I first saw this was that you could supply the same code you write in your app to a tool that the library provides, and it lets you visualize the behavior. Which means you could model and code at the same time. How cool is that!

The documentation for the library is just brilliant.

Explaining Statecharts concepts and how to use XState deserves a complete post of its own, so I won’t do it here. We will just use our example problem to see how we can solve it using statechart modeling.

For that, we need to have some basics

Terminology: States , Events , Transition and Actions .

, , and . The golden rule is, at any point in time, your app can only be in one State

An Event causes a Transition from one State to another(we generally call it action in Redux)

Actions, unlike in Redux, are the side-effects executed as we transition from one state to another(we will not talk much about them in this post)

Approach

We first identify the number of States that are possible for our problem.

that are possible for our problem. Then we identify the Transitions each state can take, and the Events that each state can listen to.

each state can take, and the that each state can listen to. Using all this we construct a configuration called Machine . A machine is like our brain, it tells us what to do. Like we dispatch an action in Redux, we send an event to the Machine and depending on the state in which it is in, it either processes and transitions the app to a new state or simply ignores the event.

For the above example, here is a simple machine I’ve modeled(have also provided a snippet on how to read the notation). The problem we had was letting the FETCH_NEXT event fire when we are in savingState . If you see the code(line 18 to 23), when the app is in savingState , we are only listening to SAVE_SUCCESS or SAVE_FAILED , so even if you fire FETCH_NEXT it will be ignored.

Now let us go a little further. If it is actually the requirement, we can even have it listen to FETCH_NEXT in the savingState and still be fine. The reason is, FETCH_NEXT will transition you to fetchingNewState , so even if the save resolves at this stage, since we are not listening to SAVE_SUCCESS it won’t be processed and your UI will be clean.

And here is my favorite part: since we are explicitly defining what events are possible in each state, when the machine is running, based on the current state, it can tell us what are the possible nextEvents . What can we do with it?

Instead of

<Button

onClick={() => dispatch('SAVE')}

disabled={isSaving || isLoading || isValid || ..... }

/>

we can do this

<Button

onClick={() => send('SAVE')}

disabled={!nextEvents.includes('SAVE')}

/>

If you are doubting the scalability of it, remember, this is just a plain state machine. Once we understand hierarchy, parallelism, conditional transitions, communication between machines etc, it will be much more exciting to design solutions. Also, for scaling and modularization there is something called Actor Model which gels very well with statecharts. Xstate library provides it out of the box. You may want to refer to this 2018 google talk to understand more about it.

Visualize

The advantage with a declarative approach like we have here is that it not only makes it easy to visualise when you read the code, but it can also be easily turned into a visual. If we put the above machine configuration in the xstate visualizer, we can see the blueprint of your app. And the best part is — it is interactive.

So now think about this: If we can turn our code into a visual, turning a visual into a piece of code shouldn’t be too difficult, right? In fact, this has already been done. All you have to do is search: Statechart Autocoding for Curiosity Rover. Yes, the flight software that was part of Curiosity Rover launched by NASA in 2012 had been automatically generated from a statechart diagram. They say that they had 10 statecharts intercommunicating(with approximately 100 states) for Auto-maneuvering the spacecraft. You can check out this talk for more details, but I’ll just mention two things they state from their experience working with statecharts:

Easier to accommodate late-breaking changes. Forces the developer to consider off-nominal scenarios

I have not tried it yet, but YAKINDU, a popular statechart modeling tool, has recently announced that they are generating Xstate code.

Observations:

This approach changes the way you think about your app. You don’t simply see it as a bunch of components tied together but the states that the user can be in. This gives a common language to communicate with designers and business analysts. A lot of assumptions can be minimized. Since we have all the logic living in one place, it becomes easy to switch rendering engines if we want to. Since we have clearly demarcated states and paths, it becomes easy to analyse what routes a user is taking in the app to improve the experience or even customize the behaviour for him. We have a term for it — Adaptive UIs

The good thing about this approach is that you don’t have to adopt it in toto. You can use it only to model some complex interactions and the rest of the app can still be in your Redux, if you want it that way.

Go give it a try! Though this is going to completely change the way you look at your app, your coding will remain much the same, so much so that you can still use your redux devtools! As David Khourshid puts it, xstate is “reducer with rules.” Yes, that makes all the difference.