Skip to content

Picking the UI Stack

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

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 just used BEM (Block Element Modifer) to organize my plain CSS. For most of my professional career, I had used BEM to organize my CSS.

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 it was rapidly be adapted by the industry, I gave Tailwind a chance.

I started slowly removing some 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 to for me to spend time thinking about what to name my CSS classes. After a few weeks of working Tailwind, I chose to migrate my whole project over to Tailwind and have not looked back since.

There is a small drawback moving from BEM to Tailwind. Since all the class names are utility based, it can sometimes be hard to figure out some HTML element is used for, whereas BEM if done correctly will at least give you an idea of what that element is used for. For example, there may be a div used for an image container, in BEM you would see something like image_container, but in Tailwind, you'll just see a bunch of CSS 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 career, because it provides a very good DX (developer experience). 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 sample code, you might have noticed that the HTML, CSS, and Javscript/Typescript are all in one 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 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 has helped 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 over React is the syntax. In React, you use JSX to write your code, whereas in Vue, you use mainly standard HTML and Javascript to write your code. To handle conditinals and loops, you use special built-in directives like v-if and v-for to do so. If you are fimilar with Angular code, it's very similar to that minus the boilerplates 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>

Vue's composition API DX is more pleasent than React's hooks API. Since Vue's reactivity similar 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 my technical requirements changed as the codebase grew, I decided to switch over to Nuxt 3. Nuxt 3 is a meta framework that is based off of Vue 3, so most of my easily migrated over.

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

SSR works by rendering the content on the server, then send the content to the client to hydrate. During this hydration process, things like event listeners are added.

While Nuxt does remove a lot of the complexity of working with SSR, adding SSR to an app still comes with additional commplexity. One of the biggest problems that I encountered while working on a SSR app is hydration mismatch. This occurs when HTML on the server is different from the client. For example, lets say you have code that generates a random number. The server will generate some random number, sends it to the client, the client then runs that same logic, and now suddenly you have an HTML mismatch. Debugging these problems get very complicated.

Urql

I looked at the Apollo and Urql library to communicate with my GraphQL (which I'll cover in another blog post). I needed something simple, but supports graph caching.

I chose to go with Urql because it has an overall smaller bundle size, yet has all the features I needed. I started out using document based caching, where it simply caches the GraphQL queries, which stores the literal JSON response of the query. As my app grew more complex, I switched over to graph caching which stores in the query result in a flat lookup table with a key/value pair.

Babylon

For 3D rendering, there are two major options here: Three.js and Babylon. Three.js is widely used, while Babylon is less known. Instead of choosing the industry standard Three.js, I went with Babylon because I felt that it would be a better fit for my project. Babylon is a full game engine, whereas Three.js is used more often for general 3D rendering.

One of the advantages of Babylon is there are less boilerplate code that I have to write for things like ray intersection. And since my app behaves more like a game, it's easier for me to map some of the concepts over to Babylon since it's a game engine. Another big thing that stands out with BabylonJS is the great support from the community. Whereever I had a question, I would post a forum and there was usually a response from one of the main Babylon developer.

Tanstack Virtual

For my app, users can create many posts and comments. I needed a way to virtualize my posts and comments. This simply means that if there are a hundred post, not all the posts should be in the DOM at the same time. This would have a negative effect on the user experience. Instead with virtualization, only a small number of posts/comments would be rendered at a time.

There are a few options when it comes to vitualization libraries, but I chose Tanstack Virtual because it supports variable heights, since you don't know what the height of post and comment elements will be before the user creates them.

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.