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"):
- 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.
- The card, name and button animate downwards towards the bottom container and become transparent.
- The now "matched" card and name appear at the top position of the bottom container with an "Unmatch" button in between.
- 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.
- 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.
Top comments (7)
I see (and agree) with most of your points. But with regard to declarative syntax, as it applies to dynamic displays (the kind that have swipes, drag-n-drop, etc.), I'm not sure if I see it as an "issue" in the same way that you do.
You can do dynamic UIs in a declarative syntax... sorta. All you're really "declaring" is the initial state of the UI. All the other dynamic stuff happens at runtime. But I don't feel like that's really much of a problem - because that's essentially the same thing that happens when you code imperatively. You're still just defining the initial state. Once the user starts interacting with the UI, the base code from which the page/app/whatever was rendered is no longer a one-to-one representation of what's on screen.
It seems like part of your quandary comes down to the fact that, in a declarative framework like React, a lot of those actions/events/transitions feel like they're kinda hidden away from us, auto-magically. In the Time of jQuery, we would have to explicitly bind and define any events that we wanted to happen. This could be laborious - but I'll freely admit that sometimes "laborious" is better, if it means that we have more explicit control of everything that's going on. (Kinda like how hardcore C/C++ devs actually LIKE the manual memory management that is required in the language.)
Because html is declarative it indeed only lets you describe a single state. Usually it is the initial state. Sometimes it is the final state (for elements that animate into the screen, for example as the user scrolls).
What I am trying to say is that frameworks like React don't lend themselves at all to creating UIs like the one I show in the video. I am not saying it is impossible to build it with React but it will get in your way all of the time and you will need to constantly work around it.
You might think the app in the video is lame but think about other examples: google maps (how it zooms in and out and flies around with animation when you search); or Trello like drag and drop that makes room as the dragged item hovers over.
There is actually a react library that does proper drag and drop, beautiful implementation, super imperative under the hood (as it has to be) with global event listeners and other React no-no's.
I've used the "react beautiful d-n-d" package (which is every bit as "beautiful" as its name implies) and of course, it's chuck full of imperative stuff as well.
Some of React's shortcoming are actually self-inflicted by the community. In other words, there's this ever-increasing trend to try to wash all of the "logic" out of components. They rant about the "evil"
class
keyword. And they worship at the alter of pure functions. And now they're in deep, spiritual, romantic love with hooks (which is funny, cuz hooks are basically a way to use functions - while keeping all of the horrible state stuff that everyone wanted to see removed from their classes). But all of that "logic" and all of that "state" in the components is what makes the declarative aspects of therender()
function work. Or, to put it another way, everything in thatclass
that's not in therender()
function is the imperative spice that makes the declarative dish appealing.But you're correct that the "faults" of the framework go beyond coding practices and dogma. Some of them are baked right into the fact that React - like every other web-development framework before it - is still jumping through all kinds of hoops to make it "feel" like you have a state-ful environment... but the underlying nature of the web (i.e., HTTP[S] requests) is that it's an inherently state-LESS protocol.
What really made Flex cool wasn't anything to do with ActionScript or FlexML. It was the Flash player. (Obviously, we all know the faults of that same player - but in this context, it was incredibly powerful, because it provided its own runtime environment.)
To make components that aren't forced to live in a box model driven by declarative layout, you'd have to make those components reasonably "self-aware". In other words, those components would have to be more like... objects.
Imagine if your UI components were, let's say, SVGs. And each SVG didn't really "know" about whatever other SVGs already existed on the canvas. But they'd have the logic to "see" if anything was in close proximity (let's say... 50 pixels in all directions). Then, when you drag one of those SVGs somewhere else, as it comes in proximity with other SVGs, each one would perform its own checks to try to scoot outta the way in as graceful a manner as possible. (With many extra style points if that motion was also calculated under a physics-based engine of Newtonian movement.)
Interesting idea about those environment aware objects...
I don't have anything smart to add to it atm but interesting
But as for your discussion on transitions and other "advanced" UI features, I absolutely agree with you. I have a whole post lined up in my head about how we (meaning, the whole universe of JS devs) need to stop thinking of our apps from a 2002 perspective. Soooo many of the React apps I see still lazily cling to a page model, in which a whole bunch regular ol' form fields are littered about. There's rarely any intelligent thought dedicated to thinking about the best possible way to facilitate user interaction and to guide the user through the app.
Exactly 👏 👏 👏
I am kind of blaming templating (= declarative) here. HTML itself is declarative so I can see how it evolved from it. With that said HTML was not originally intended for building real apps.
The box model also doesn't help (forces the designer to think in rectangles rather than more interesting shapes, unless they are willing to CSS the $#!t out of everything).