Tanker og erfaringer
fra en frontend-udvikler

Vue 3 Tips & Best Practices i 2026

Skrevet af
Nicky Christensen Nicky Christensen
Udgivet
Opdateret
Læsetid
9 min
vuevue3typescriptfrontendbest-practices

Vue 3 har udviklet sig enormt siden de første releases. Med Vue 3.4 og 3.5 har vi fået en række nye features, der gør det nemmere og mere elegant at bygge moderne webapplikationer. I denne artikel deler jeg mine bedste tips og best practices baseret på 19+ års erfaring med frontend-udvikling.

Script Setup er standarden

Hvis du stadig bruger Options API eller den lange setup() funktion, er det tid til at skifte. <script setup> er den anbefalede måde at skrive Vue 3 komponenter på. Det giver dig renere kode, bedre TypeScript-understøttelse og automatisk eksponering af variabler til din template.

<script setup lang="ts">
import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    Count: {{ count }} (doubled: {{ doubled }})
  </button>
</template>

Alt der er deklareret i <script setup> er automatisk tilgængeligt i din template. Ingen return-statement, ingen defineComponent wrapper. Rent og enkelt.

defineModel - to-vejs binding gjort nemt (Vue 3.4+)

En af de bedste tilføjelser i Vue 3.4 er defineModel(). Før skulle du manuelt håndtere props og emits for v-model bindings. Nu er det en one-liner:

<!-- ChildInput.vue -->
<script setup lang="ts">
const modelValue = defineModel<string>()
</script>

<template>
  <input v-model="modelValue" />
</template>
<!-- ParentForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildInput from './ChildInput.vue'

const name = ref('')
</script>

<template>
  <ChildInput v-model="name" />
  <p>Du skrev: {{ name }}</p>
</template>

Du kan også bruge navngivne models og flere models på samme komponent:

<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
</script>

defineSlots - typesikre slots (Vue 3.3+)

Med defineSlots() kan du definere typer for dine slots, hvilket giver fuld TypeScript-understøttelse og autocompletion:

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { item: string; index: number }): any
  header(props: { title: string }): any
}>()
</script>

Det er særligt nyttigt når du bygger genbrugelige komponenter, hvor andre udviklere skal vide præcis hvilke slot props der er tilgængelige.

Reactive Destructuring (Vue 3.5+)

Vue 3.5 introducerede reactive destructuring af props - en feature mange har ventet på. Før mistede du reaktiviteten, når du destructurede props. Nu virker det bare:

<script setup lang="ts">
const { title, count = 0 } = defineProps<{
  title: string
  count?: number
}>()

// title og count er stadig reaktive!
// Du kan bruge dem direkte i computed, watch, etc.
</script>

Det gør koden mere læsbar og giver dig mulighed for at sætte default-værdier direkte i destructuring.

Composables - den rigtige måde at genbruge logik

Composables har fuldstændig erstattet mixins i Vue 3. De er nemmere at teste, har bedre TypeScript-support og undgår navnekollisioner. Her er et praktisk eksempel:

// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })

  return data
}
<script setup lang="ts">
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('theme', 'light')
const favorites = useLocalStorage<string[]>('favorites', [])
</script>

Gode composable-principper:

  • Navngiv dem altid med use-prefix
  • Return refs eller reactive objekter så forbrugeren kan destructure
  • Hold dem fokuserede på én ting
  • Håndter cleanup i onUnmounted hvis nødvendigt

Pinia fremfor Vuex

Vuex er officielt deprecated. Pinia er den anbefalede state management-løsning for Vue 3. Den er lettere, har bedre TypeScript-support og bruger Composition API:

// stores/useUserStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const isLoggedIn = computed(() => user.value !== null)

  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    })
    user.value = await response.json()
  }

  function logout() {
    user.value = null
  }

  return { user, isLoggedIn, login, logout }
})

Brug altid setup store syntaksen (som ovenfor) fremfor options syntaksen. Den er mere fleksibel og mapper direkte til Composition API patterns, du allerede kender.

TypeScript med Vue 3

Vue 3 er bygget med TypeScript, og understøttelsen er førsteklasses. Her er de vigtigste patterns:

<script setup lang="ts">
// Typer dine props med generics
const props = defineProps<{
  title: string
  items: BlogPost[]
  variant?: 'primary' | 'secondary'
}>()

// Typer dine emits
const emit = defineEmits<{
  submit: [value: string]
  update: [id: number, data: Partial<BlogPost>]
}>()

// Typer dine refs
const inputRef = ref<HTMLInputElement | null>(null)
const posts = ref<BlogPost[]>([])

// Brug interfaces til komplekse typer
interface BlogPost {
  id: number
  title: string
  content: string
  publishedAt: Date
}
</script>

Et godt tip: Definer dine typer i separate filer under en types/ mappe, så de kan genbruges på tværs af komponenter og composables.

Performance Tips

Brug shallowRef til store objekter

Hvis du har store objekter eller arrays, der ikke har brug for dyb reaktivitet, kan shallowRef spare dig for betydelig overhead:

import { shallowRef, triggerRef } from 'vue'

// Kun top-level .value ændringer trigger opdateringer
const largeList = shallowRef<DataItem[]>([])

// Når du opdaterer nested data, trigger manuelt
largeList.value[0].name = 'Updated'
triggerRef(largeList)

// Eller erstat hele værdien
largeList.value = [...largeList.value]

Computed vs methods

computed caches resultatet og genberegner kun når afhængigheder ændres. Brug det altid til afledte værdier:

// GODT - caches resultatet
const expensiveResult = computed(() => {
  return items.value.filter(i => i.active).sort((a, b) => b.score - a.score)
})

// SKIDT - kører ved hver re-render
function getExpensiveResult() {
  return items.value.filter(i => i.active).sort((a, b) => b.score - a.score)
}

Lazy loading med defineAsyncComponent

Split din bundle ved at lazy-loade tunge komponenter:

import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// Med loading og error states
const HeavyEditor = defineAsyncComponent({
  loader: () => import('./components/HeavyEditor.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 10000
})

Almindelige fejl du bør undgå

1. Muter ikke props

Vue advarer dig, men det er værd at gentage. Klon altid data fra props før du ændrer den:

const props = defineProps<{ user: User }>()

// FORKERT
props.user.name = 'New name'

// RIGTIGT
const localUser = ref({ ...props.user })

2. Glem ikke at bruge i v-for

Unikke keys hjælper Vue med effektivt at opdatere DOM:

<!-- Brug altid en unik identifier, aldrig index -->
<div v-for="item in items" :key="item.id">
  {{ item.name }}
</div>

3. Undgå watch når computed er nok

Mange udviklere bruger watch hvor en computed ville være bedre:

// OVERKOMPLICERET
const fullName = ref('')
watch([firstName, lastName], ([first, last]) => {
  fullName.value = `${first} ${last}`
})

// BEDRE
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

4. Håndter cleanup i composables

Hvis din composable opretter event listeners eller timers, ryd altid op:

export function useWindowResize() {
  const width = ref(window.innerWidth)

  function onResize() {
    width.value = window.innerWidth
  }

  onMounted(() => window.addEventListener('resize', onResize))
  onUnmounted(() => window.removeEventListener('resize', onResize))

  return { width }
}

Opsummering

Vue 3 i 2026 er et modent og kraftfuldt framework. De nyeste features som defineModel, reactive destructuring og forbedret TypeScript-support gør det til en fornøjelse at arbejde med. Fokuser på at bruge <script setup>, skriv composables i stedet for mixins, vælg Pinia til state management og brug TypeScript overalt.


Har du brug for hjælp med din Vue 3 applikation, eller overvejer du at modernisere din eksisterende frontend? Tag kontakt med mig - jeg hjælper gerne med alt fra arkitektur og code reviews til fuld implementering.

Har du brug for en erfaren udvikler?

Lad os tage en snak om hvordan jeg kan hjælpe med teknisk strategi, arkitektur og frontend-udvikling.

Kontakt mig