Reddit’s Voting UI in Vanilla vs React vs Vue vs Hyperapp: shedding light on the purpose of SPA frameworks

July 18, 2018 0 Comments

Reddit’s Voting UI in Vanilla vs React vs Vue vs Hyperapp: shedding light on the purpose of SPA frameworks

 

 

Reddit Voting UI

Many beginners may wonder what the purpose of SPA (Single Page Application) libraries/frameworks is. Why can’t they just use plain old JavaScript since it can achieve the same thing without the bloat? What is the purpose of them, and why are they so hyped up?

In their very essence, these libraries are designed to make building interactive web applications easier. Users frequently interact with websites in the form of liking posts, uploading images, chatting with friends, posting comments, and more. This interactivity involves updating the DOM elements to reflect the new data when the user interacts with the page.

For example, a “like” button may have two JavaScript states: unliked and liked. It will probably have CSS states defined too, like :hover, :active, and :focus. CSS states are easy to manage, but the JavaScript states can get hairy. To let the user know they successfully pressed “like”, the button changes appearance permanently, regardless of whether they’re hovering over it, pressing it, or have it focused. And they need to be able to undo this action as well, while data is sent off to the server to let the application know what the user did. This process of updating elements is often time-consuming and prone to bugs, especially in medium to large applications.

But, you can’t just take my word for it. You’ll need to go through the experience with examples to truly understand the why. That’s what this article is about! In addition, there will be a comparison of the libraries and how they achieve the same thing, and which one shines the most 🌟.

Let’s dive right in with a practical example, starting off with our trusty native library: Vanilla.js.

The website reddit.com has a voting UI that consists of two buttons and a score: an upvote button that increases the score by 1 and a downvote button that decreases the score by 1. The score is a number given to a post or comment as an indicator of its quality as voted by users. The logic behind this UI element is actually more complex than you might initially assume. For the purposes of simplicity, the server request aspect of this UI will be omitted.

At its most basic, the HTML markup may look like this:

Reddit Voting Markup

When the user presses the upvote button, we want the text inside the h1 element to increase by 1. Conversely, when they press the downvote button, we want it to decrease by 1.

To do anything with these elements, we need to select them using document.querySelector() so we can use them in our JavaScript code.

We want to react when the user clicks the buttons. To do that, we’ll need to add some event listeners to them.

Now, what exactly should go within these listener functions? To understand, let’s break down what happens when we interact with the UI.

Let’s say we have a post that has a score of 0.

  • We press upvote. What happens? The score is now 1.
  • We press upvote again. What happens? The score is now 0. Why? Because we can’t keep upvoting forever like a counter. Pressing it again means we’re “undoing” our vote, resetting it back.
  • We press downvote this time. What happens? The score is now -1.
  • We press upvote again. What happens? The score is now 1. Notice that the score increased by 2 , from -1 to 1. This part can be a bit tricky.

Throughout this interaction, we need to keep two variables alive: the score, and the user’s own vote. Both of these values work together to determine what happens when they press one of the buttons. This concept is called state, and it describes the data, or memory, of our application. How things behave is dependent on the current state of the application.

This works like a counter for now, but we’ll start off slow. We get the current score by throwing the textContent of the scoreHeading element into the Number() constructor, which ensures we have a number that works sanely with the math operators + and - . We then set the textContent to either the current score plus 1, or minus 1, depending on which button we clicked.

But this isn’t doing what we want yet 😢.

We need to be able to “undo” what we voted on. This is where state comes into play. We need to have some source of “truth” that tells us the current state of the app. Using vanilla, this is frequently done by storing the data in the DOM itself, often as a class. So, why not?

We’re going to use the class active to determine if the button has been pressed and is the user’s current vote.

If the button’s class contains active, then we know that they are undoing their vote, so we need to take 1 off the score. If it doesn’t, we know that they haven’t pressed it yet, so we’ll add the class and add 1 to the score.

Now to the second piece of the puzzle. How do we handle when they have already voted, but are now choosing the opposite vote? As mentioned before, this results in a difference of 2 in the score.

As you can see, we need another branch that checks if the opposite button is currently active. If it is, we need to remove its class, add the to our upvote, and then add 2 to the score. We also need to repeat this for the downvote button by doing the opposite, and we end up getting this very long chunk of code.

Hmm, that’s a lot of code, and repetitive at that. Can’t we DRY (Don’t Repeat Yourself) that up? Well yes.

Here’s our whole code, DRY and all (well, except for repeating the active class).

Vanilla Reddit Voting UI

You might be wondering what’s so bad about this. Many software developers consider this approach, called imperative programming, to be vastly inferior to its cousin, declarative programming. These might seem like fancy terms, but they’re really not:

  • Imperative programming: how to do something with the steps involved.
  • Declarative programming: what something should be or do. The steps are abstracted away.

As you can see, our approach belongs to the former category, because we have to manually update the DOM elements when the user interacts with the buttons in order to reflect the changes. This is a step-by-step process that involves lots of manual checks and assignment. This type of programming can lead to more bugs because there’s more work to do.

Wouldn’t it be great if we could just say, “We want the UI to look like this when certain variables are these values”? You already do this with CSS.

transition: opacity 0.5s;

All you need to do is declare you want to make the element’s opacity property to transition for half a second whenever it changes values. Just like that, it works. You don’t need to manually update the opacity with some kind of tweening algorithm and requestAnimationFrame. The browser does all the hard work for you, and you don’t need to worry about a thing (except when there are browser bugs, which sadly there’s been tons of over the years).

Back to these things. How can they make our code more declarative, so that we don’t need to touch the DOM when we update our data? Learn the APIs 🤓.

In each one, there are four essential concepts that are disguised by their API:

  • state: an object containing the data/memory/raw information of our app.
  • actions: functions or methods that update the state.
  • view: the UI or visual representation of the state, as presented to the user.
  • DOM lifecycle: our app is mounted to the DOM and the elements are changed (created, updated, destroyed) when the user interacts with the app. Whenever the state changes, the view is updated and the DOM needs to be changed to reflect it.

Now that we have these core concepts out of the way, let’s jump straight into the React version of our voting UI, because that’s the most popular according to npm downloads.

After importing the necessary React modules, we create a class and initialize the state of our component.

  • vote is the user’s current vote. 0 if no vote, -1 if downvoted, 1 if upvoted.
  • score is the score of the post.

We now have a state object that represents our application, but we have no way of showing it to the user. Let’s use a React component’s render() method for that.

The destructuring is done to minimize the line width for the demonstration, it’s not really necessary.

React has a few idiosyncracies such as using an HTML-like syntax in JavaScript, called JSX, and uses DOM property names like className instead of class. This is because those little HTML tags are actually JavaScript being sneaky in disguise 🤡. For now, you don’t need to know why, we’ll get to that in a bit later.

This may look a bit weird:

className={vote = 1 ? 'active' : undefined}

It contains an important concept: based on the state, the button’s class is some string. We’re using a ternary operator so that the class is 'active' if the user’s vote matches the button, or has no class at all because it evaluates to undefined. This is declarative because we’re describing what the class should be based on the state, not how to manually update it.

Anyway, once we’ve mounted our component to the DOM so we can actually see it, we’ll be greeted with a familiar sight. Except nothing happens, because we have no interactivity! We haven’t added event listeners to the buttons that change the state when the user presses them, which is kind of the whole point.

To add an event listener in React, you use an attribute on our sneaky HTML element in the form of onEvent. In our case, we’re concerned with click, so we want onClick. This is semantic, so it reads, “on [user] click, run this function that does something”. In our case, the “something” is a function that changes the state of the vote component that our UI will reflect back to the user.

We need to add a method to our component called vote() which is a nice little verb that describes what is happening. We should bind anonClick listener to our elements that will call this method and pass in a value, either 1 representing an upvote, or -1 representing a downvote.

To update the state, which in turn always updates the UI, we use an inherited method from React.Component called setState() (which is why we’re extending the component). When we call this function and pass in an object, it merges it with the current state object to produce a new state. In our case, we’re passing in a function that returns a new partial state object, because accessing the current state is done most reliably this way.

Setting state.vote:

vote: state.vote = type ? 0 : type

Note that type (the parameter of our vote() method) is always either 1 (upvote) or -1 (downvote). And vote is a property of the state which describes the user’s current vote, which can be three values: 0 if no vote, 1 if they already upvoted, and -1 if they already downvoted.

We want to set state.vote to a new value by taking into account the current state. If the current vote is the same as the one we just passed in to the method, it means the user is undoing their vote and therefore it should be reset back to 0 to indicate they have no vote anymore. Otherwise, we should just set it to that value.

By adding their vote to the original score, we’re left with the final result of the score + their vote affecting it.

This leaves us with relatively concise code, and our vote component is complete.

React Reddit Voting UI

Now onto Vue. Although its essential concepts are the same as React, the way it goes about it is a bit different.

Since we’ve already gone through the concepts step-by-step using examples with React, I’m just going to present the entire Vue instance straight away.

Instead of using class syntax, Vue uses a plain object. In our case here, we’re passing the object directly to the Vue() constructor, instead of specifying a component with Vue.component() or using single-file components. The core ideology remains the same regardless.

  • data is equivalent to state. It contains the raw information ready to be converted to a pretty UI for users.
  • methods is an object containing the functions used to update the data. In React, you can just define these as methods in the class itself.
  • template is equivalent to render(). It’s the UI — a representation of the data. The core difference however, is that Vue’s is a string template containing HTML with Vue add-ons, while React’s is actually JavaScript function calls disguised as HTML, basically React add-ons. The consequence of this is that Vue uses special attributes like @ to add an event listener. In React, JavaScript expressions are placed within the curly braces {} , while in Vue, the expressions exist with a binding attribute : and placed within the string contents of the attribute.
  • el is the element to mount the app to. This syntax is far more concise than having to call ReactDOM.render() , but they serve the same purpose.

Apart from the template differences, you will also notice the vote() method is different.

vote(type) {
this.voteType = this.voteType === type ? 0 : type
}

It’s basically the same logic as our React component, just wrapped in a different coating. What you’ll notice here is that Vue relies on mutation. It uses “watchers” (getters/setters) to determine when the object’s properties change. Instead of calling a special method called setState() like React, we directly mutate the instance itself.

Additionally, we can’t just use vote as our data property name, because our vote() method is hogging it in the instance’s namespace. Vue sticks all the data properties and methods on the same top-level namespace of the instance. This doesn’t really matter that much, but it’s still a name collision you need to be aware of.

Vue Reddit Voting UI

Last but not least. (I know there are plenty of other ones, but this one deserves the spotlight).

Hyperapp is a new SPA framework, with its first version released in early 2017. Compared to React (2013) and Vue (2014), it’s quite new and hasn’t gained nearly as much community traction. But to someone seeking a beautiful API and a neat software development experience, all bundled in a tiny size (less than 2kB gzipped), Hyperapp is one of the best experiences for building a web app.

At the core of Hyperapp is a functional, mostly pure approach to building web applications. There are no stateful or class components — they don’t have their own local state (they work like React’s functional components). The entire state of an application is described by a single plain JavaScript object, globally represented throughout an app.

Once again, the logic of the Reddit Voting UI is basically the same, just wrapped in a new coat of paint.

Both the state of the app and actions used to update it are plain JavaScript objects. The view is a function that receives the state and wired actions (invoking them results in a view update). Due to this, the variables defined outside of the scope aren’t used.

The view uses JSX like React, but tries to stay closer to the actual HTML by favoring class over className, onclick over onClick etc. Like React though, JSX is not actually necessary, but is generally preferred for the same reasons React promotes it over React.createElement().

Hyperapp Reddit Voting UI

Unlike React and Vue, there is no concept of “local state” in Hyperapp. It is global state-driven: you can’t create many instances of a component that all have their own encapsulated data.

Hyperapp avoids this hidden state. It wants everything explicit and visible to everything in the app at all times. It does this by using a global state. This has two consequences:

  • Omniscient views: Components anywhere in the view know about any part of the state of the application at any given time. There are no issues communicating state between components, because they all have the same knowledge.
  • State-driven: In React and Vue, you can typically just throw a component in the app anywhere and it will work because of local state. In Hyperapp, you need to make sure it has its state defined within the global state before putting a component somewhere.
  • Vanilla techniques involve pre-existing or SSR elements that are selected by JavaScript and given “hooks” (event listeners) for reactivity. The elements are imperatively updated to match new state, and state is stored in the DOM itself. The alternative is to create the elements using vanilla JS, have a state object etc, but you soon realize that constructing a view using Element.append() is verbose and you lose all the declarative benefits.
  • React, Vue and Hyperapp all share a similar ideology: state-driven apps where the UI is automatically updated efficiently by the library when the state changes. No more imperative manipulation of DOM elements to match the state.
  • React and Hyperapp share similarities such as using plain JavaScript (function calls, usually with JSX) to construct views. This gives you the full freedom JavaScript expressions and logic. On the other hand, Vue opts for its own templating language using directives in the HTML template to decide how the view should look, making it feel more traditional and accessible to beginners.
  • React and Hyperapp encourage immutability, while Vue encourages mutation. It’s well known that immutable operations are more reliable than mutable ones and cause fewer bugs, so this is one of Vue’s strongest pitfalls. Beginners especially may spend hours scratching their heads when their view doesn’t seem to be updating.
  • React and Vue allow for local state — encapsulated data that doesn’t affect other component instances and is hidden from other components. Hyperapp, on the other hand, only has global state that represents the entire view. State management becomes easier with this method, but you may find it difficult when it comes to managing things like lists when an individual item’s state needs to be updated.


Tag cloud