Scaffolding an App with Vue 3, Nuxt and TypeScript
Tags: webdev, vue, typescript
Published: 2021-03-28 18:20:55

For most of my projects I use [my own, simple setup](https://iamschulz.com/basic-components-how-to-framework-your-site/) that takes a step back from large, JavaScript-based web apps. One limitation that comes with it is sharing state between multiple screens. My setup doesn't do soft navigations by design, so it has to rely on `localStorage`, `sessionStorage`, and `indexedDB` for persistent states. That becomes cumbersome very quickly and I acknowledge that some projects absolutely benefit from reactivity and the automagic mechanisms, that the likes of Vue and React bring with them.

Setting up such a project can be tricky, especially if you don't know the entire ecosystem of your chosen framework and simply want to get on with a little side project. Here's how and why mine a wired together - from choosing a tech stack to implementing a persistent store.

<!--more-->

# Vue, not React

I got into Vue before taking a look at React and I think that alone plays the largest role in that decision. I find Vue more [intuitive](https://twitter.com/IamSchulz/status/1314172474377080838) to use. Vue's [Single File Components](https://vue-loader.vuejs.org/spec.html) easily wrap up a component in a concise, human-readable way. Also, there's the [Composition API](https://v3.vuejs.org/api/composition-api.html) with its `setup()` and `reactive()` mechanics, which are an absolute joy to use. However, using Vue (or React or Angular in that case)brings one huge drawback: It's naturally Client-Side Rendered - meaning the actual document you serve to the browser is nothing but a link to a huge JS file, without which your site simply won't display anything at all. This has all sorts of bad [implications](https://iamschulz.com/in-defense-of-the-separation-of-concerns/#rendering-everything-in-javascript), from a UX point of view to Performance.

I'd like to pre-render my Vue project on the server and push that to the browser. The tool to do that would be [Nuxt](https://nuxtjs.org/).

# Set up Nuxt

Nuxt is a framework that builds on top of Vue. One of its key features is [Server Side Rendering](https://nuxtjs.org/docs/2.x/concepts/server-side-rendering). It creates fully populated documents from your Vue components and serves those. Those documents look like the finished website but don't behave like it yet. All the logic is still packaged into JS bundles and sent to the browser separately. As soon as the JS initializes, it adds the usual Vue functionality to the site. This mechanic is called [Hydration](https://www.youtube.com/watch?v=JKL4q_NeDK4) and helps with Vue's performance implications.

Using Nuxt is likely a key decision at the very start of the project because it's so fundamental to the structure and build process. Switching to Nuxt later in the development process probably involves some [big rewrites](https://www.youtube.com/watch?v=xCGu5Z_vaps).

Installing Nuxt is described very well [in their own documentation](https://nuxtjs.org/docs/2.x/get-started/installation). I usually go the `create-nuxt-app` way, because that takes most of the setup process out of my hands and provides well-working presets for a PWA.

# Use Vue 3 with Nuxt

At the time of writing, Nuxt (2.15) still uses Vue 2 by default, but it provides a node package that exposes the Composition API:

```bash
yarn add @nuxtjs/composition-api
```

In order to use Vue's new features, you don't import its components from `vue`, but from `@nuxtjs/composition-api` instead:

```javascript
import {
	defineComponent,
	inject,
	computed,
	onMounted,
	ref,
} from "@nuxtjs/composition-api";
```

# TypeScript

Using TypeScript is optional. You can skip this step entirely if you like. Not every project absolutely needs TypeScript.

I found that using it gives me some confidence in my code because it forces me to think my data structures through before implementing them. In plain JavaScript, I used to just code it. I may have been faster in prototyping, but maintaining my progress further down the development process became ever more painful and slow. Whenever I needed to extend a data structure, it involved a lot of refactoring. With TypeScript, I tend to write cleaner structures, to begin with. I find Refactoring easier because my types serve as documentation for themselves. I'll use TypeScript in my code snippets in this article - beware when copypasting.

However, it also provides some arbitrary hoops to jump through and increases the complexity of the project. Simply hacking your Vue components together as with Vanilla JS won't work. Here are some things to look out for:

## Specify the language

You need to specify `<script lang="ts">` in your component, to actually use TypeScript in Vue.

## Typing props

Vue [already uses type primitives](https://vuejs.org/v2/guide/components-props.html#Prop-Types) in props. Those translate into TypeScript seamlessly.

```javascript
props: {
    id: {
        type: Number,
        required: true
    },
    name: String
},
```

But TypeScript is capable of a lot more than that. In order to use [Interfaces](https://www.typescriptlang.org/docs/handbook/2/objects.html) as prop types, you can cast an Object primitive as an Interface like that:

```typescript
interface Person {
    id: number;
    firstName: string;
    lastName: string;
    registered: boolean;
}

...

props: {
    person: {
        type: Object as () => Person
    }
}
```

Now your component will only accept correctly typed Person objects in its properties.

## Typing Refs

[Refs](https://v3.vuejs.org/api/refs-api.html#refs) are Vue's most simple technique to make things reactive.

```typescript
const foo = ref<string | number>("foo");
```

They're also unique in their ability to [work with DOM elements](https://codingexplained.com/coding/front-end/vue-js/accessing-dom-refs). But whenever the DOM and strict types meet, chaos ensues. DOM Refs are declared with `ref(null)`. The ref value is only filled when the component renders. Before that, the value is (as we declared) null. After it renders, it is populated with the HTML element we set it to in the component template. But we still don't know _what kind_ of an HTML element it is in the script section, so TypeScript won't let us use its API yet. To fix that, let's type the ref when we set it:

```html
<textarea ref="userInputEl"></textarea>
```

```typescript
const userInputEl = ref<HTMLTextareaElement | null>(null);
userInputEl.value?.setSelectionRange(0, 10);
```

# Build your own store

Mario Brendel wrote a [really nice article](https://medium.com/@mario.brendel1990/vue-3-the-new-store-a7569d4a546f) on how we might not need VueX in Vue 3 anymore. It boils down to using the Composition API to `provide` a `reactive` object across multiple components.

![A diagram describing how the app provides a store reactive object that gets injected into athe app's components.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3tt2h755rsyye51vjnlh.png)

He shares some code snippets after which I model my stores on as well. The nice thing about that technique is that you don't have to deal with a behemoth like VueX or Redux. Instead, you build your own store exactly to your needs. A very simple implementation could look like this:

```typescript
import { Person, persons } from "@/data/persons.json";
import { Cat, cats } from "@/data/cats.json";
import { Dog, dogs } from "@/data/dogs.json";

interface StoreData {
	persons: Person[];
	cats: Cat[];
	dogs: Dog[];
}

export class Store {
	protected state: StoreData;

	constructor(readonly storeName: string) {
		const data = this.data();
		this.state = reactive(data);
	}

	protected data() {
		return {
			persons,
			cats,
			dogs,
		};
	}

	public getPersons(): Person[] {
		return this.state.persons;
	}

	public getCats(): Cat[] {
		return this.state.persons;
	}

	public getDogs(): Dog[] {
		return this.state.persons;
	}

	//...and all the other store logic
}

export const store = new Store("DataStore");
```

This will give me a class that can return a slice from a reactive object. That's basically all I ever wanted out of Vue. In order to use it across the whole app, we can use the Composition API's `provide`/`inject` methods: A base component high up at the root of the component tree provides the store and all its children can inject it.

```typescript
// the app's base component
import { defineComponent } from "@nuxtjs/composition-api";
import { store } from "@/store/store";

export default defineComponent({
	provide: {
		store,
	},
});
```

```typescript
// a component that needs access to the store
import { defineComponent, inject } from '@nuxtjs/composition-api';
import { Store } from '@/store/store';

export default defineComponent({
    setup() {
        const store = inject('store') as Store;
        const persons = store.getPersons();
    }
}
```

## Persisting with localStorage

Now, the store is constructed whenever the App loads. That's fine for soft navigations, but a hard reload or following a hard link would clear it. That's fine if your store only keeps information like if a drawer menu should be opened or closed, or if it only loads static data from a JSON file anyway. But if you store a large user input like a filled form, it'd be very annoying to let that clear just because the user reloads the page.

`localStorage` (or `sessionStorage` or `indexedDb`, depending on your needs) comes to save the day! When the store initializes, we check if the browser has already some data cached and use that. If not, the store initializes with empty states.

```typescript
import { FormData } from "@/data/formData.js";

interface StoreData {
	formData: FormData;
}

export class Store {
	protected state: StoreData;

	constructor(readonly storeName: string) {
		const data = this.data();
		this.state = reactive(data);
	}

	protected data() {
		const localStorage = process.browser
			? window.localStorage
			: {
					getItem(): string {
						return "";
					},
			  };

		return {
			formData: localStorage.getItem("formData") || new FormData(),
		};
	}

	protected persist(key: "formData"): void {
		if (!process.browser) {
			return;
		}
		localStorage.setItem(key, String(this.state[key]));
	}

	public getFormData(): FormData[] {
		return this.state.formData;
	}

	public setFormData(payload: FormData): void {
		this.state.formData = payload;
		this.persist("formData");
	}

	//...and all the other store logic
}

export const store = new Store("FormStore");
```

This example uses a `persist()` method on every setter that updates the `localStorage` with current data. Using a browser API like `localStorage` in Nuxt can be tricky because our App might be server-side rendered. That way the store would be initialized, while we're still in the node environment of the server. Client-side storages are not available here (and even if they were, the server would have no way of knowing its content). That's why we check for `process.browser`. It returns `true` if we're client-side and have access to the browser APIs. While on the server, we mock a storage API and its `getItem()` method to only return an empty string. It's fortunate that `localStorage` only stores strings anyway, it keeps our mock simple.

# Looking back

Each of those steps adds complexity to the project, which is something I watch very critically. Whenever I spin up a side project, I tend to work on it for a few days and then leave it for months until I don't know anymore how it works. Keeping the stack complexity down is important to me.

That's why every step of my stack is optional. Building just a little Proof of Concept and TypeScript is standing in your way? Leave it out. Your App is too small to justify a store? Don't implement one then. Most importantly though: Don't need Vue? Just ignore this article altogether and use a more fitting stack.