Skip to content

Picking the UI Stack

In this post, I'll break down the reasons why I chose my current UI stack for my project, Zagrock.

Requirements

I wanted my UI stack to be simple, but I should also be able to learn something new. In addition, I wanted to make sure my content is easily queried by search engines, since the application mainly focuses on user generated content.

BEM vs Tailwind

When I first started coding Zagrock, I used BEM (Block Element Modifier) to organize my plain CSS. For most of my professional career, I had used BEM to structure my styles.

Around this time, I started hearing more and more about a utility-first CSS framework called Tailwind. At first, I was a bit skeptical and wasn't sure if I wanted to add another framework to my stack.

After following Tailwind's development and seeing how rapidly it was being adopted by the industry, I decided to give it a try.

I slowly started removing some of my BEM CSS in favor of Tailwind, and to my surprise it sped up my development time. Tailwind has a great VSCode extension that helps with autocompletion. It also removed the need for me to spend time thinking about what to name my CSS classes.

After a few weeks of working with Tailwind, I decided to migrate my entire project over to it and have not looked back since.

There is one small drawback when moving from BEM to Tailwind. Since the class names are utility-based, it can sometimes be harder to tell what an HTML element is used for. With BEM, if the naming is done correctly, the class name usually gives you an idea of the element’s purpose. For example, there might be a div used for an image container. In BEM you might see something like image_container, but in Tailwind you'll just see a list of utility classes instead.

Javascript Stack and Libraries

I'll dive a bit into what powers my UI stack.

Vue 3/Nuxt 4

Vue 3 is a web framework that is similar to Angular and React. It was created by Evan You (also the creator of Vite). I chose this framework over React and Angular, both of which I have used in my career, because it provides a very good developer experience (DX).

A simple Vue 3 component looks something like this:

<script setup lang="ts">
import { ref } from 'vue'
const greeting = ref<string>('Hello World!');
</script>

<template>
  <p class="greeting">{{ greeting }}</p>
</template>

<style scoped>
.greeting {
  color: red;
  font-weight: bold;
}
</style>

In this example, you might notice that the HTML, CSS, and JavaScript/TypeScript are all in a single file. This is known as a Single-File Component in Vue.

Besides being the recommended approach to writing Vue code, this approach makes it easier for me to read the code since everything is in one place. When I worked with large React codebases in the past, it was sometimes hard to figure out which CSS classes mapped to a specific component. Tailwind helps reduce the need to figure this out, but I still think Vue's approach is cleaner.

Another thing that I really like about Vue 3 compared to React is the syntax. In React you use JSX to write your code, whereas in Vue you mainly use standard HTML and JavaScript. To handle conditionals and loops, Vue provides built-in directives such as v-if and v-for.

If you are familiar with Angular templates, it's very similar to that but without the boilerplate associated with Angular.

An example usage of directives:

<script setup lang="ts">
import { ref } from 'vue'
const showDog = ref<boolean>(false);
</script>

<template>
  <div v-if="showDog">I am a dog</div>
  <div v-else>I am a cat</div>
</template>

I also find Vue's Composition API more pleasant to work with than React's Hooks API. Since Vue's reactivity is opt-in, it's easier to reason about when writing reactive code.

Originally when writing Zagrock, I simply used Vue 3 without Nuxt. But as the project codebase grew and my technical requirements changed, I decided to switch over to Nuxt. Since Nuxt is a meta-framework built on top of Vue, most of my code migrated over easily.

One of the main reasons for using Nuxt is that it has built-in server-side rendering (SSR). Since content is rendered on the server, it allows for faster initial loading times, makes it easier for pages to appear in search results, and removes the complexity of writing SSR code manually.

SSR works by rendering the content on the server and then sending the generated HTML to the client. The client then hydrates the page by attaching event listeners and activating interactivity.

While Nuxt removes a lot of the complexity of working with SSR, adding SSR to an app still comes with additional challenges. One of the biggest problems I encountered while working on an SSR app is hydration mismatch. This occurs when the HTML generated on the server is different from the HTML generated on the client.

For example, imagine you have code that generates a random number. The server generates one number and sends it to the client, but when the client runs the same code it generates a different number. Suddenly you have an HTML mismatch. Debugging these problems can get very complicated.

Urql

I looked at both Apollo and Urql to communicate with my GraphQL API (which I'll cover in another blog post). I needed something simple but with good caching support.

I chose Urql because it has a smaller bundle size while still providing all the features I needed.

I initially started using document-based caching, where the library simply caches the GraphQL queries and stores the literal JSON response. As my app grew more complex, I switched over to graph caching, which stores query results in a normalized key/value structure.

Babylon

For 3D rendering, there are two major options: Three.js and Babylon. Three.js is widely used, while Babylon is less known.

Instead of choosing the industry standard Three.js, I decided to go with Babylon because I felt it would be a better fit for my project. Babylon is closer to a full game engine, whereas Three.js is often used for more general 3D rendering.

One advantage of Babylon is that there is less boilerplate code required for things like looking for ray intersection. Since my app behaves more like a game, it was easier for me to map some of my concepts to Babylon since it's structured as a game engine.

Another thing that stood out with BabylonJS is the strong community support. Whenever I had a question and posted on the forum, there was usually a response from one of the main Babylon developers.

Tanstack Virtual

For my app, users can create many posts and comments. I needed a way to virtualize these lists.

This means that if there are hundreds of posts or comments, not all of them should be in the DOM at the same time. Rendering everything would negatively affect performance and the user experience. Instead, with virtualization only a small number of posts or comments are rendered at any given time.

There are several options when it comes to virtualization libraries, but I chose TanStack Virtual because it supports variable heights. Since users generate the content, you don't know what the height of a post or comment will be ahead of time.

Toolings

Tooling is pretty straightforward for my app. I just use the industry standard ESlint for linting and formatting my code. I use the built in Nuxt dev tools for debugging. For testing, I use Vitest.

Building, formatting, linting, and testing are also automated through Github Actions.

That's all I have my why I chose my UI stack for today. In my next post, I would like to go over some of the technical details of my avatar customization system.