Guide

Vue.js fundamentals explained

Your operations team needs a live dispatch board that updates when drivers check in — without reloading the page or wiring dozens of DOM listeners by hand. Vue.js is a progressive JavaScript framework built around a fine-grained reactivity system: declare reactive state once, bind it in an HTML-like template, and Vue keeps the DOM in sync automatically. Vue 3 (the current major version) pairs approachable templates with the Composition API for reusable logic, ships with Vite for fast dev builds, and powers dashboards at Alibaba, GitLab, and thousands of Laravel + Inertia stacks. This guide covers Vue’s reactivity primitives, template directives, components and single-file modules, composables, a Harbor Fleet dispatch dashboard worked example, a framework decision table, common pitfalls, and a production checklist alongside our React fundamentals guide, TypeScript fundamentals guide, and frontend state management guide.

What Vue is (and how it differs from React)

Vue is a view layer — like React, it renders UI from data. The architectural difference is how reactivity works:

  • Vue tracks which reactive properties each component reads during render and re-runs only affected subtrees when those properties change — similar to signals or MobX.
  • React re-renders a component function when state updates and relies on reconciliation to diff the virtual tree.

Vue offers two component authoring styles. The legacy Options API groups code by option (data, methods, computed). The modern Composition API (recommended for new projects) groups code by feature inside <script setup> blocks — closer to React hooks but with explicit reactivity wrappers. Both compile to the same runtime; pick Composition API unless you maintain a large Options API codebase.

Vue is progressive: you can sprinkle it onto a static page via a CDN script for one widget, or scaffold a full SPA with Vue Router, Pinia, and Vite. That incremental adoption path is why many PHP and Rails teams adopt Vue before committing to a full rewrite.

Reactivity: ref, reactive, computed, and watch

Vue 3’s reactivity is built on proxies. Primitives (strings, numbers, booleans) wrap in ref(); objects use reactive(). Access values in script with .value on refs; templates unwrap refs automatically.

import { ref, computed, watch } from 'vue'

const driverCount = ref(12)
const trips = ref([])

const activeTrips = computed(() =>
  trips.value.filter(t => t.status === 'en_route')
)

watch(driverCount, (newCount, oldCount) => {
  console.log(`Fleet size ${oldCount} → ${newCount}`)
})

Computed vs methods

computed properties cache their result and recompute only when dependencies change — use them for derived state (filtered lists, formatted totals). Methods re-run on every render; reserve them for event handlers and actions that mutate state.

watch vs watchEffect

watch(source, callback) runs the callback when a specific ref or reactive property changes — ideal for side effects tied to one input (fetch when search query changes). watchEffect auto-tracks every reactive read inside its callback and re-runs when any dependency shifts — convenient but harder to reason about in large components. Prefer explicit watch for API calls and analytics.

A common beginner mistake: destructuring a reactive object loses reactivity (const { name } = reactive(user) breaks tracking). Use toRefs() or access properties directly on the reactive object.

Templates and directives

Vue templates are valid HTML with extra attributes prefixed by v-:

  • v-if / v-else / v-show — conditional rendering (v-if removes from DOM; v-show toggles CSS display).
  • v-for — list rendering; always provide a stable :key (trip ID, not array index).
  • v-model — two-way binding on inputs; shorthand for :value + @input.
  • v-bind (:) — bind HTML attributes or component props to reactive expressions.
  • v-on (@) — attach event listeners.

Event modifiers shorten common patterns: @click.prevent calls event.preventDefault(); @keyup.enter fires only on Enter. For forms, combine v-model.trim and .number modifiers to sanitize input before it hits state.

Vue 3 supports multiple root elements per template (fragments) — no wrapper <div> required. Dynamic components use <component :is="currentView" /> for tabbed interfaces without routing.

Components: props, emits, and slots

Components are reusable Vue instances defined in .vue single-file modules (SFCs) or inline. Parent-to-child data flows through props (one-way down); children signal parents through emits.

<!-- TripCard.vue -->
<script setup>
const props = defineProps({
  trip: { type: Object, required: true },
  highlight: { type: Boolean, default: false }
})
const emit = defineEmits(['assign', 'cancel'])
</script>

<template>
  <article :class="{ highlight }">
    <h3>{{ trip.destination }}</h3>
    <button @click="emit('assign', trip.id)">Assign driver</button>
  </article>
</template>

Slots let parents inject markup into child components — default slots for body content, named slots for headers/footers, and scoped slots that pass child data back up (v-slot:item="{ trip }"). Slots are Vue’s answer to React’s children prop but more flexible for layout components like data tables and modals.

Define prop types and defaults in defineProps for runtime validation in dev builds. In TypeScript projects, use generic type arguments: defineProps<{ trip: Trip; highlight?: boolean }>().

Single-file components and the build toolchain

A typical .vue file has three blocks:

  • <script setup> — component logic (imports, state, composables).
  • <template> — declarative markup.
  • <style scoped> — CSS limited to this component via data attributes.

Vite is the official dev server and bundler. It serves source modules natively in development (instant HMR) and rolls up production assets with Rollup. Create a project with npm create vue@latest — the scaffold prompts for TypeScript, Router, Pinia, ESLint, and Vitest.

Vue Router handles client-side navigation with route records, lazy-loaded chunks, and navigation guards for auth. Pinia is the official store — simpler than Vuex, with TypeScript inference and devtools support. For server state (API caching, pagination), pair Pinia with TanStack Query for Vue rather than storing fetch results in global stores (see our state management guide).

Composables: reusable logic

Composables are functions that use Vue’s reactivity APIs and can be shared across components — the Composition API equivalent of custom hooks:

// useFleetPolling.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useFleetPolling(intervalMs = 5000) {
  const trips = ref([])
  let timer

  async function refresh() {
    const res = await fetch('/api/trips/active')
    trips.value = await res.json()
  }

  onMounted(() => {
    refresh()
    timer = setInterval(refresh, intervalMs)
  })
  onUnmounted(() => clearInterval(timer))

  return { trips, refresh }
}

Name composables useSomething by convention. Keep them focused: one composable for WebSocket connections, another for form validation. Lifecycle hooks (onMounted, onUnmounted, onBeforeUnmount) only work when called synchronously during setup — not inside setTimeout or async callbacks after an await.

Worked example: Harbor Fleet dispatch dashboard

Harbor Fleet operates a regional delivery network. Dispatchers need a browser dashboard showing active trips, available drivers, and ETA alerts — updating every few seconds without full page reloads.

Project structure

A Vite + Vue 3 + TypeScript app with Pinia for UI preferences (column layout, dark mode) and a useFleetPolling composable for REST polling (WebSocket upgrade planned). Routes: / (live board), /history (completed trips), /drivers (roster management).

Live board component

DispatchBoard.vue calls useFleetPolling(3000) and renders a TripCard per trip with v-for and trip UUID keys. Computed unassignedTrips filters trips lacking a driver ID. Clicking “Assign” opens a modal (DriverPicker) that emits selected; the parent POSTs to /api/trips/:id/assign and optimistically updates local state before the next poll confirms.

Forms and validation

New trip intake uses v-model on address fields with @submit.prevent. Client-side rules (required destination, valid weight) live in a useTripForm composable returning errors refs. Server validation errors map to field-level messages via a 422 response handler.

Deployment

npm run build outputs static assets to dist/, served by nginx with try_files fallback to index.html for history-mode routing. API requests proxy to a FastAPI backend on the same domain to avoid CORS. Vitest component tests cover TripCard emit behavior; Playwright smoke-tests assign flow against staging.

Framework decision table

Need Prefer Why
Largest hiring pool and ecosystem React More libraries, jobs, and Next.js for full-stack SSR
Approachable templates + gradual adoption Vue 3 HTML-like syntax; drop onto existing pages; gentle learning curve
Laravel or Rails backend with SPA frontend Vue + Inertia Official Laravel pairing; server-driven routing without separate API
Smallest bundle, compile-time reactivity Svelte / SvelteKit No virtual DOM runtime; excellent for content sites
Enterprise structure, full framework Angular Opinionated modules, DI, built-in forms and HTTP
Dashboard with heavy forms and tables Vue 3 + component library (PrimeVue, Vuetify) Rich data-grid and form widgets out of the box
Content site with minimal interactivity Astro or SSG Ship less JS; hydrate Vue islands only where needed

Common pitfalls

  • Mutating props — props are read-only; copy to local state or emit an event so the parent updates the source of truth.
  • Missing :key on v-for — index keys break when lists reorder; use stable IDs to preserve component state and animations.
  • Reactive loss on destructure — destructuring reactive() objects strips tracking; use toRefs() or dot access.
  • Async setup without Suspense — top-level await in setup requires a parent <Suspense> boundary or the component will not render until resolved.
  • Storing server data in Pinia — duplicated cache invalidation logic; use TanStack Query for remote state and Pinia for UI-only state.
  • Overusing watchEffect — implicit dependencies cause surprise re-fetches; prefer explicit watch for API side effects.
  • Mixing Options and Composition APIs inconsistently — pick one style per codebase; migrating file-by-file creates two mental models.

Production checklist

  • Scaffold with npm create vue@latest and enable TypeScript + ESLint from day one.
  • Use <script setup> and Composition API for all new components.
  • Define prop types and emit signatures; run vue-tsc in CI.
  • Extract polling, auth, and form logic into composables with unit tests.
  • Lazy-load route components with () => import('./views/History.vue').
  • Configure Vite build.rollupOptions.output.manualChunks to split vendor libraries.
  • Set base in vite.config if deploying under a subpath.
  • Enable Vue devtools in staging; disable verbose logging in production builds.
  • Test critical flows with Vitest + Vue Test Utils and one Playwright E2E path.
  • Document when to reach for Pinia vs local refs vs TanStack Query in your README.

Key takeaways

  • Vue 3 tracks reactive dependencies automatically — you declare state, bind templates, and the DOM updates.
  • ref, computed, and watch are the three primitives to master before building larger apps.
  • Single-file components with <script setup> are the standard authoring format; Vite is the default toolchain.
  • Props down, emits up, slots for flexible composition — same unidirectional data flow as React.
  • Composables extract reusable logic; pair Pinia with TanStack Query instead of caching API responses in global stores.

Related reading