Conceptual Gaps in Declarative Frontend Frameworks - Part 2 - Animations and Transitions Are Not "Nice To Have"

Isaac Hagoel - Feb 25 '20 - - Dev Community

Welcome to part two in this series. The intro to part one is relevant here as well. I won't repeat it. I recommend you read it if you're interested in my motivation for writing this series and some of my background (I use React professionally yada yada).
Let's jump straight into the action.

A Short Case Study

I'll start by showing you a section of a web application I was working on a while ago in my spare time (a.k.a "small side project"). It has all sorts of bells and whistles, but for the purpose of our discussion, we will focus on this one section. Apologies in advance for the (lack of good) styling.

The idea was to create a party game in which each player matches the names of the other players with answers they have provided to a bunch of questions. In this example, the question is "what's your favorite food and why?".
Before you continue reading, have a quick watch. The video is only 35 seconds long and has no sound (no need for headphones 😌).

Let's break it down:

  • There are two main containers: one for unmatched cards and names (that the user still needs to match) and the other for already matched ones.
  • The top container has two rows (swipeable horizontally) - one for cards and one for names. Each row can be scrolled to the left and right independently. The cards flip on tap.
  • In the top container, between the two rows there is a fixed "Match" button.
  • The bottom container has one column (swipeable vertically). Each element in the container is made of a card, a name and an "Unmatch" button in between them.
  • When the "Match" button is clicked few things happen. Each step takes place when the previous step completes (a.k.a "staggered"):
    1. The button becomes transparent and disabled. The name animates upwards and the card downwards so that they close the gap and "attach" to one another.
    2. The card, name and button animate downwards towards the bottom container and become transparent.
    3. The now "matched" card and name appear at the top position of the bottom container with an "Unmatch" button in between.
    4. In the top container, the card to the left of the (now) missing card animates to fill the gap. If there is no card to the left, the card to the right does it. The names do the same.
    5. The counter at the bottom of the screen updates it's "left to match" message (it turns to a "submit" button when there are zero left to match).
  • The "unmatch" button acts similarly to the "match" button just the opposite, kind of (as you can see in the video). I won't break it downs to save some of your time 😪

What I want you to notice is that all of these sequential animations and events are essential in order for the user to be able to keep track of the process that is taking place. Remove any of them and elements start jumping around in a chaotic manner.

A Mental Exercise

Let's say we wanted to implement something like this using a declarative framework like React. How would we go about it?
Most developers I know would immediately start googling for libraries. I am pretty sure that even with an animation library this will prove to be quite tricky but for our purposes, I would like us to do it without a library.
Normally, in declarative style, we would try to create a lot of boolean state variables that express that a part of the process is taking place. They would have names such as isLoading.
We would then use them to conditionally render elements (for example, a spinner). This approach won't work here for the most part, because conditional rendering is not what we're after. Our problem involves moving stuff around in a highly coordinated matter.
mmm.... anyway let's proceed...
For the animations we would normally use CSS transitions and animations (possibly with delays) that would be triggered by adding and removing classes. We need to coordinate those with adding and removing elements from the top and bottom container somehow. Damn, another timing issue. Nevermind.. moving on...

We can try to achieve the sequence by scheduling all of the future state changes (not good because the user can take an action that should break the chain) or better, maybe we could link them in a sequence somehow using await, then or callbacks. Once we do that though, we are not declarative anymore. Do A then B then C lands strictly in imperative-land and imperative === bad, right?

Also, what exactly is the right place for this coordination logic? Is this a part of the rendering cycle? Can it be thrown away and recalculated on every render? I would say "Not at all".
Oh well...

Another thing to think about - the parent of the bottom and top container will need to orchestrate cutting and pasting (with some conversion) state items (names and cards) between the two containers. It will need to do so in perfect sync with the animations (Svelte has a neat built-in way to deal with simple cases of this).

Now is a good time to ask: Is it even possible to express this kind of sequence declaratively? I invite you to prove me wrong but I don't see how.

Do you know why?

  • Most of the interesting bits here happen in the transitions between states. In other words, if we think about this application as a graph with bunch of states (nodes) and arrows pointing from one state to another (edges), the complexity here is in the arrows.
  • Declarative state is a snapshot frozen in time. It is static by design. You can sprinkle some CSS on top to make it appear somewhat dynamic (fading elements in and out etc.). You can add some boolean state variables for simple, isolated cases (ex: "isSubmittingForm") but at the end of the day you are dealing with isolated points in time.

Frameworks like React don't (and probably can't) give us proper tools to describe processes and transitions. They give us frames (states) without a timeline to put them on in order to turn them into a movie (the best we can do within their declarative bounds is a comics strip 😞).
This has some serious implications...

Chicken and Egg

"Okay", you might say, "but how often do we actually need to create A UI like this? We normally just need radio buttons, selects, input boxes and other form elements for interactivity.".
Well, what if I told you, that the very reason most single web "applications" are nothing but glorified forms - is the nature of the tools we use in order to build them?

Think about it for a moment... is JSX fundamentally different to the backend templating languages that were used in the "old web" (that mainly consisted of static pages and forms)?

Remember how websites used to look like in the glory days of flash? People did all kinds of crazy, experimental and occasionally beautiful $#!t.
I don't miss flash but have you ever wondered why we don't have these kind of experimental UIs anymore?
I think our declarative tools and state of mind are at least partially to blame.

That's it for this time. Thanks for reading.
I will be happy to hear your thoughts.

P.S

In case you wonder, the side project I used as an example for this post was written in vanilla Javascript. I went vanilla mainly because I wanted to get a better understanding of the browser APIs and the limits of the platform.

. . . . . . . . . . .