loading...

State Driven Development for User Interfaces (Part 1: An introduction)

nimmo profile image Nimmo Updated on ・6 min read

Like a lot of companies at the moment, my workplace has a lot of Angular (1.x) in our codebase, and we'd prefer not to write much more. That isn't a criticism of Angular directly of course, but I think it's fair to say that front-end development has moved on a lot since it first appeared, and that we have better options now than we did all those years ago.

We've got a couple of applications that we've developed with React and Redux, and we've decided that that's going to be our default for new UI code.

To help the roll-out of these things run smoothly for our engineering team, we wanted to come up with a structured approach for working with them. In order to achieve this, my colleague Todd and I have been deliberating over what we consider to be the biggest problem with UI development in general and how best to solve it.

What is the problem?

Basically, we believe that an application's state (or rather the possible states it can have) should be the starting point in our development process, but we feel that state is often mistakenly considered to be a mere side effect of any actions within our applications.

Picture the scene: you've just been added to a full-stack team. The team is responsible for an e-commerce application, but the back-end is their main focus. They had an experienced UI engineer, but the engineer in question was hit by bus last week and as a result is currently unavailable, which is why you were added to the team. You have UI experience, and you're here to help. Your first task is to add a new component that displays a sign-up promotion if the user is not logged in, and the most recent items bought by the user if they are logged in.

You've got a design, so you decide you might as well start by coding that up. Once you can see what you're working with you'll feel like you're making progress, after all.

You work your way through the markup and figure out where your new component is supposed to live. You add it in, you make sure that the right bits of it are displayed when the user is logged in, and that the right bits are displayed when the user is logged out, and you're done. Next.

You're probably still thinking "okay, but what's the problem?". And it is hard to see, since everything seems to be fine. But what's happened here is:

  • an application's architecture has been affected by a small visual component
  • the overall complexity has increased
  • the ease with which you can reason about the application has decreased

...all at the same time. Not only with this component, but with everything that was ever added this way.

So, what can we do differently?

This could have been approached from the opposite end entirely, by considering the application's state (or rather, possible states) first.

Let's break down the task we had earlier:

Add a new component 
that displays a sign-up promotion if the user is not logged in, 
and the most recent items bought by the user if they are logged in

All we know about this application that is useful to us right now, based on this task alone, is that it can exist in one of two states:

LOGGED_IN
LOGGED_OUT

And what do we know about the component that we're being asked to add? We know that the form it takes is completely different depending on which state it's in. Hang on, does this sound like it should be one component?

An engineer I worked with many years ago used to say that an "and" in a unit test description is telling you that you have two tests. I'd suggest that the "and" in our task description here is telling us that we're dealing with two components. The fact that they happen to be in the same position on a given page is completely irrelevant, but that wasn't so obvious when we were only thinking about how this needed to look.

Consider how this looks now, based on what we know so far:

possibleStates: [
  { 
    name: 'LOGGED_IN',
    RenderedComponents: [RecentItems]
  },
  { 
    name: 'LOGGED_OUT',
    RenderedComponents: [SignUpPromotion]
  }
]

Now that we have a nice clear conceptual separation, we find that everything feels easier to work with, and that we have two tasks that could easily be worked on simultaneously. Or at least this would be true, if the entire application had been coded in such a way in the first place.

Other benefits

The biggest benefits that we've found we get from working in this way include:

  • Reduced cognitive load
  • Ease of testing
  • Ease of visual documentation
  • Close ties with Behaviour Driven Development (BDD) techniques

Reduced cognitive load

Thinking about state above all else means that you're able to think about individual application states in isolation, knowing for certain that nothing you're thinking about currently has any impact on any other states, other than potentially sending messages from one state to another (which is something we'll discuss in part 3 of this series).

Ease of testing

Because State Driven Development (SDD) leaves us with well-defined paths through our application tree, snapshot testing is very easy to accomplish. We feel that the number of tests we even have to think about writing is massively reduced by being hyper-aware of our different state types at all times, and being able to plug those directly into our snapshot tests.

Ease of visual documentation

Because SDD leads to everything being carefully compartmentalised, it's very easy to provide a visual representation of what's going on. For example, here's a diagram of an application tree for the task we discussed earlier:

State diagram showing paths for LOGGED_IN and LOGGED_OUT

This shows a unidirectional data flow through our application, starting at our authentication store, and showing the path through to the UI itself (including the data to be represented) depending on whether a user has logged in or not.

Most of us don't love writing documentation, but it's hard to argue with its value when it comes to describing our application's architecture to people, or when it comes to reminding ourselves of it. My allusion to the bus factor earlier in this post wasn't coincidental; I believe that SDD makes it easier to reduce your team's bus factor thanks to this.

Close ties with BDD techniques

The more we thought about it, the more it became obvious that a combination of state and actions is mapping out the behaviours of your application. This may not be an especially shocking revelation, but it's something that is easy to forget when you're still thinking of state simply as "something that happens".

We're big fans of Dan North (the creator of BDD). You might be too. If so, you may be familiar with this quote from Introducing BDD: “Programmers wanted to know where to start, what to test and what not to test, how much to test in one go, what to call their tests, and how to understand why a test fails”.

SDD moves beyond this stage, allowing us to easily define and build the structure of our application by breaking it down into manageable application tree paths that are based on behaviours which have been translated into state. Whilst this has less value to the non-developer members of our teams than BDD (due to it not being based in Ubiquitous Language) it does add a lot of value to the developer. We feel that it is a solid methodology that makes for a very easy jump between documentation and implementation.

Implementation

In order to make this simpler, we've also come to the conclusion that thinking about our applications in terms of finite state machines (FSMs), rather than thinking about possible interactions from users, has improved our ability to reason about our applications, as well as making a big difference to how we plan them out. More about that in Part 2: Finite State Machines For UI Development.

Links to things I've mentioned:

Discussion

pic
Editor guide
Collapse
nimmo profile image
Nimmo Author

As a tiny aside for anyone who happens to already know my thoughts on Elm, our decision not to use it is in no way indicative that those thoughts have changed. There are some wider contextual reasons why Elm's introduction into my current workplace doesn't feel like a good move, and none of them are to do with Elm itself. However, this methodology fits perfectly with Elm development too. :)