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-ifremoves from DOM;v-showtoggles CSSdisplay).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; usetoRefs()or dot access. - Async setup without Suspense — top-level
awaitinsetuprequires 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
watchfor 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@latestand enable TypeScript + ESLint from day one. - Use
<script setup>and Composition API for all new components. - Define prop types and emit signatures; run
vue-tscin 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.manualChunksto split vendor libraries. - Set
baseinvite.configif 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, andwatchare 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
- React fundamentals explained — the closest alternative with hooks and JSX
- TypeScript fundamentals explained — typing props, emits, and composables
- Frontend state management explained — when Pinia, local state, or server caches fit
- CSS fundamentals explained — styling scoped SFC blocks and design tokens