Vue: the mental model
Baseline: Vue 3.5 stable, 3.6 in RC. See ../../STACK_BASELINE.md.
The entry point for this folder — what Vue is, the single-file component, and the reactivity model everything else builds on. Depth lives in the numbered siblings; see README.md for the map.
What Vue is
A progressive framework: it works as a script tag on one page, and as a full application framework with an official router, store and meta-framework. That range is deliberate and is why it shows up both in enterprise admin panels and in incrementally-modernised legacy apps.
Unlike React, Vue ships opinions. Routing is Vue Router, state is Pinia, SSR is Nuxt, styling is scoped <style> in the component file. You spend no time assembling a stack.
The single-file component
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>
<template>
<button @click="count++">{{ count }} / {{ doubled }}</button>
</template>
<style scoped>
button { font-weight: 600; }
</style>
Three blocks in one file: logic, markup, styles scoped to this component. The .vue file is compiled — nothing here is interpreted at runtime, and the compiler uses the template’s structure to optimise updates. See 19_compiler_and_rendering.md.
<script setup> is the current authoring style. Top-level bindings are automatically exposed to the template, there is no return object, and imported components need no components: registration. Anything you read that uses export default { setup() { ... return { ... } } } is the older, more verbose form.
Reactivity
This is the concept the rest of Vue hangs off. A ref is a box whose .value is tracked; reading it inside a render function, computed or watch registers a dependency, and writing it re-runs whatever read it.
const count = ref(0); // .value in script, auto-unwrapped in template
const user = reactive({ name: 'Ada' }); // proxy; access properties directly
Three consequences worth internalising early:
- You mutate state directly.
items.value.push(x)is correct. The proxy sees the mutation. This is the opposite of React, where identity change is how updates are detected. - There are no dependency arrays. Vue records what was read; you never declare it. See 04_computed_watch_watcheffect.md.
- Only components that read the changed value re-render. A parent re-rendering does not cascade to children whose props did not change.
Use ref for everything, including objects. reactive loses reactivity when destructured or reassigned, which is the trap people hit in week one — see 03_ref_vs_reactive.md and 02_reactivity_internals.md.
Templates
Templates are HTML plus directives. The essential set:
<p v-if="user">{{ user.name }}</p>
<p v-else>Not signed in</p>
<li v-for="item in items" :key="item.id">{{ item.label }}</li>
<input v-model.trim="query" :disabled="loading" @keyup.enter="search" />
<form @submit.prevent="save">
: is v-bind, @ is v-on, and modifiers like .prevent, .trim, .enter handle the boilerplate that React makes you write by hand. {{ }} interpolation is always escaped; only v-html is unsafe. Full treatment in 18_template_syntax_and_directives.md.
Component communication
| Direction | Mechanism |
|---|---|
| Parent to child | props — defineProps<{ id: number }>() |
| Child to parent | events — defineEmits<{ select: [id: number] }>() |
| Two-way | defineModel() |
| Parent to any descendant | provide / inject |
| Parent supplies markup | slots, including scoped slots |
| Across the app | a Pinia store |
defineProps, defineEmits, defineModel and defineExpose are compiler macros, not imports — they are erased at build time, which is why they must be called at the top level of <script setup>.
Details: 07_props_emits_vmodel.md, 08_slots_and_scoped_slots.md, 09_provide_inject.md, 10_pinia.md.
Lifecycle
onMounted(() => { /* DOM exists */ });
onUnmounted(() => { /* clean up listeners, timers, subscriptions */ });
The Composition API registers hooks by calling them during setup, so they must be called synchronously — registering onMounted inside an await or a callback silently does nothing. Effects created by watch/watchEffect inside a component are disposed automatically on unmount. See 06_lifecycle_hooks.md.
Logic reuse
A composable: a plain function using reactivity APIs, named useSomething, returning refs and functions. There are no rules-of-hooks constraints because it is not identified by call order — it is just a function call. VueUse is the standard library of them. See 16_composables.md.
Starting a project
npm create vue@latest # official scaffolder: TS, router, Pinia, Vitest, ESLint
npx nuxi@latest init my-app # Nuxt, when you need SSR/SSG and server routes
Vite is the build tool in both cases — same author as Vue, and the reason Vue’s dev experience is fast by default. Add vue-tsc --noEmit to CI, because Vite strips types without checking them. See 17_typescript_with_vue.md.
Interview angle
- “What is Vue’s reactivity system?” - Proxy-based dependency tracking. Reading a reactive value inside an effect registers it; writing re-runs the effects that read it. That is why there are no dependency arrays and no manual memoization.
- “Why can you mutate state in Vue but not React?” - Vue detects change through the proxy, React through reference identity.
arr.push(x)is correct Vue and a no-op in React. - “
reforreactive?” -reffor everything. It works for primitives, survives destructuring and reassignment, and keeps one consistent access pattern.reactivebreaks reactivity the moment someone destructures it. - “Composition API or Options API?” - Composition with
<script setup>for new code: logic composes into testable functions, TypeScript inference works properly, and tree-shaking improves. Options API still ships and is fine to maintain. - “What is
<script setup>doing?” - it is compiled into asetup()function with template bindings inlined. Thedefine*calls are compile-time macros, not runtime functions, which is why they cannot be called conditionally.