vue
Vue is a frontend framework for building web UIs with HTML-based templates and a fine-grained reactivity system. You write single-file components (.vue files) that combine template, script and scoped styles, and Vue tracks exactly which state each piece of the DOM depends on, so updates are automatic and precise. It scales down to a script tag progressively enhancing a server-rendered page and up to full SPAs with the official router, Pinia for state, and Nuxt for SSR. MIT licensed, led by Evan You with a paid core team.
Technically excellent, stable, and easier to learn than React, with first-party tooling that removes whole categories of decisions. The honest tradeoff is ecosystem and job-market size, not the framework itself.
Use it if
- You want HTML-first templates with directives (v-if, v-for, v-model) instead of JSX, which designers and backend devs pick up noticeably faster
- You need to sprinkle interactivity into an existing server-rendered app (Rails, Laravel, Django) without rewriting it as a SPA
- You want an officially maintained full stack: vue-router, Pinia and Vite tooling are first-party, so the pieces are designed together
- Fine-grained reactivity matters: Vue updates only what changed without the memoization discipline React requires
- Your hiring pool or component-library needs are React-shaped: the React ecosystem is several times larger, and many commercial component vendors and design systems ship React-only or React-first
- You need serious native mobile: Vue has no equivalent of React Native with comparable backing; the options are webview wrappers or third-party bridges
- Your team will fight about style: Options API and Composition API are both fully supported, and mixed codebases where every file picks a different one are a real maintenance tax
- You rely on the newest React-ecosystem tooling (React Server Components patterns, specific vendor SDKs); Vue equivalents exist but arrive later
Setup reality
npm create vue@latest scaffolds a Vite project with checkbox choices for TypeScript, router, Pinia and testing, and it is fast and pleasant. The friction is conceptual: refs need .value in script but not in templates, and destructuring a reactive object silently kills reactivity, which bites every newcomer once. You must also pick Composition versus Options API up front (pick Composition with script setup for anything new). SSR is not in the box; that is Nuxt or manual @vue/server-renderer wiring. Editor support means installing the Vue extension (Volar); without it .vue files are inert text.
Patterns
Single-file component with script setupsfc-component
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
<style scoped>
button { font-weight: bold; }
</style>script setup is the modern default; everything declared top-level is available in the template with no return statement.
ref vs reactivereactive-state
import { ref, reactive } from 'vue'
const count = ref(0)
count.value++ // .value needed in script, not in templates
const state = reactive({ user: { name: 'Ada' } })
state.user.name = 'Grace' // deep reactivity, no .valueDestructuring reactive() breaks reactivity: const { user } = state gives a plain snapshot. Default to ref for everything if in doubt.
Derived state with computedcomputed
import { ref, computed } from 'vue'
const items = ref([{ price: 10 }, { price: 5 }])
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price, 0)
)Computeds cache until a dependency changes; never mutate state inside one.
React to state changeswatchers
import { ref, watch, watchEffect } from 'vue'
const query = ref('')
watch(query, async (q, oldQ) => {
results.value = await search(q)
})
watchEffect(() => {
document.title = `Search: ${query.value}`
})watch takes an explicit source and gives old/new values; watchEffect auto-tracks whatever it reads and runs immediately.
Typed props and eventsprops-emits
<script setup lang="ts">
const props = defineProps<{ title: string; count?: number }>()
const emit = defineEmits<{ (e: 'save', id: number): void }>()
function onClick() {
emit('save', 42)
}
</script>defineProps and defineEmits are compiler macros, no import needed; use withDefaults() for prop defaults with the type-only syntax.
v-model on a custom componenttwo-way-binding
<!-- Child.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>
<!-- Parent.vue -->
<!-- <Child v-model="name" /> -->defineModel (stable since 3.4) replaces the old modelValue prop plus update:modelValue emit boilerplate.
Load data when a component mountsfetch-on-mount
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const posts = ref<Post[]>([])
const loading = ref(true)
onMounted(async () => {
posts.value = await fetch('/api/posts').then((r) => r.json())
loading.value = false
})
</script>Top-level await in script setup also works but makes the component async, requiring a <Suspense> boundary in the parent.
Access a DOM elementtemplate-ref
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef('search')
onMounted(() => input.value?.focus())
</script>
<template>
<input ref="search" />
</template>useTemplateRef arrived in 3.5; the ref is null until mount, so touch it only in onMounted or a watcher.
Extract reusable logic into a composablecomposable
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage(key: string, initial: string) {
const value = ref(localStorage.getItem(key) ?? initial)
watch(value, (v) => localStorage.setItem(key, v))
return value
}Composables are just functions using reactivity APIs; the use prefix is convention. Check VueUse before writing your own, it probably exists.
Pass state down without prop drillingprovide-inject
// ancestor
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
// any descendant
import { inject } from 'vue'
const theme = inject('theme', ref('light')) // second arg = defaultFine for a plugin or theme; for real app state use Pinia, which is easier to test and inspect in devtools.
Render lists and conditionalslist-rendering
<template>
<p v-if="items.length === 0">Nothing here</p>
<ul v-else>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>Always bind :key to a stable id, never the array index, or reorders will recycle DOM state; avoid v-if and v-for on the same element.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react | npm | You need the biggest ecosystem, React Native, or your hiring market demands it |
| svelte | npm | You want even less boilerplate and compiled output with no runtime framework overhead on small widgets |
| solid-js | npm | You want Vue-style fine-grained reactivity with JSX syntax and top-tier update performance |