Vue
Vue is a JavaScript framework for user interfaces whose reactivity system updates exactly the parts of the screen that read a value when that value changes. You never declare dependencies; in exchange, nearly every Vue bug that is not a typo is a value that quietly left the reactive system — copied out, destructured or replaced. The page opens with the whole Composition API on one screen, then covers tracking and the eight ways a value stops updating, computed against watch, templates and the compiler, composables, the move from Vue 2 to Vue 3, and five documented cases.
- Current version
- 3.5 stable · 3.6 in RC
- Reactivity
- Proxy, tracked on read
- Created by
- Evan You · 2014
Vue on one screen: the API a component uses
A Vue component is usually one .vue file: a template that describes the screen, and a <script setup> block where everything else lives — state, values derived from it, reactions to changes and the contract with the parent component. Here is all of the script side first, because between them these functions are almost the whole of what you write. The table lists the Composition APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms functions and compiler macros a component actually uses as of Vue 3.5 (September 2026), grouped by job, with what each one is for and the way each one gets misused. A version number marks a recent addition.
| API | What it is for | How it gets misused |
|---|---|---|
| ref | Any value that changes — a number, a string, an object; read and written through .value | Copying .value into a local variable and expecting the variable to follow |
| reactive | An object whose every property is tracked, with no .value | Replacing it as a whole or destructuring it — both leave the screen holding the old one |
| shallowRef | A large value replaced as a whole: an API response, a list of ten thousand rows | Changing something inside it — only assigning .value is tracked |
| readonly | Handing state to a child or a composable that may read it but not write it | Believing it freezes the source — the original still changes, and the view follows |
| computed | A value derived from other state, cached until something it read changes | Side effects inside — a request, a write, a log; it may run any number of times, or never |
| watch | Doing something when a named source changes: a request, storage, an animation | Assigning to other state — a computed written the long way, section three |
| watchEffect | The same, with its sources found by running it once and seeing what it reads | Reading something by accident inside it — every value it touches becomes a trigger |
| onWatcherCleanup | Cancelling what the previous run of a watcher started, such as a request (3.5) | Calling it after an await, where Vue no longer knows which watcher it belongs to |
| toRef / toRefs | Taking properties out of a reactive object without cutting them off from it | Using them on a plain object, which has nothing to stay connected to |
| toValue | Letting your own function accept a ref, a getter or a plain value alike (3.3) | Calling it once at the top of the function, so later changes are never seen |
| markRaw / toRaw | Keeping a third-party object out of the proxy — a map, a chart, a class instance | Writing through toRaw: the write succeeds and nothing on screen updates |
| nextTick | Waiting until the DOM shows a change you have just made | A general “wait a bit” — it waits for one update, no more |
| defineProps | Declaring what the component receives from its parent, with types | Writing to a prop from the child — the parent owns it |
| defineEmits | Declaring the events the component sends up to its parent | Emitting an event nobody declared, which neither TypeScript nor a reader can see |
| defineModel | Two-way binding for v-model without writing the prop and the event by hand (3.4) | Changing an object received through it in place — the parent’s object changes with no event |
| provide / inject | Handing a value down many levels without passing it through each one | A global variable in disguise: nobody can tell where the value came from |
| useTemplateRef | Reaching a DOM element or a child component from script (3.5) | Reading it before the component is mounted, when it is still null |
| useId | An id that ties a label to its input, the same on the server and in the browser (3.5) | Keys for v-for — keys come from the data |
| onMounted / onUnmounted | Starting and stopping what lives outside Vue: a listener, a timer, a chart | Adding a listener in one and forgetting the other — one leak per visit to the screen |
| defineAsyncComponent | Loading a component’s code only when it is needed — and, since 3.5, choosing when server-rendered HTML wakes up | Wrapping every component: each one is another network request |
Reading creates the dependency
Vue wraps your state in a proxy — a JavaScript object that stands in front of another one and can observe every read and every write of its properties. A ref does the same for a single value by keeping it inside an object with a .value property. That one capability is the whole system, and everything else on this page is a consequence of it.
While an effect is running — a component render, a computed value, a watcher — Vue knows which effect is current. Every property read during that run records the effect against itself. That is tracking. Later, when a property is written, Vue re-runs the effects recorded against that particular property, and nothing else. That is triggering. A component that did not read the value is not even asked, which is why a change in one corner of a large page costs the same as a change on a small one.
The machinery has also been getting cheaper. Vue 3.5 (September 2024) rebuilt the reactivity system and cut its memory use by 56%, with array operations up to ten times faster in some cases. Vue 3.6, a release candidatea build meant to become the releaseA build the team believes is ready to ship, published so users can try it before the final release. Bugs found now are fixed and no new features are added, but details can still change — so it is for testing, not for production.All terms since July 2026, rebuilds it again on alien-signals, a small library designed for exactly this kind of tracking. None of it changes the rule. It changes how many tracked values a page can afford.
Eight ways a value stops updating
None of these throws an error. The value simply stops following its source, and the bug looks like a rendering problem. The numbers are referred to further down the page.
| You wrote | What actually happened | The fix |
|---|---|---|
1. const { count } = state from a reactive object | Destructuring reads the property once and copies the number out of the proxy. | Keep state.count, or take toRefs(state) and destructure that. |
2. const { count } = props inside a setup(props) function | The same copy. Props are a reactive object like any other. | toRefs(props), or <script setup>, where since 3.5 the compiler turns count into props.count for you. |
3. watch(count, …) on a destructured prop in <script setup> | The compiler rewrites reads, but watch receives the current number, not a source. | watch(() => count, …) — pass a getter. |
4. state = reactive({ … }) a second time, replacing the whole object | The template still holds the first proxy. The variable now points at another one nobody reads. | Use a ref and assign .value, or Object.assign(state, next). |
5. useTotal(state.price) — a tracked value passed into your own function | The argument is evaluated once, so the function receives a number and never sees a change. | Pass toRef(state, "price") or () => state.price, and read it with toValue inside. |
6. list.value.push(item) on a shallowRef | A shallow ref tracks only the assignment of .value, not changes inside it — that is its purpose. | Assign a new array, list.value = [...list.value, item], or call triggerRef(list). |
7. Writing to the original object you passed to reactive() | The proxy stands in front of the original. A write that goes around it is never seen. | Write only through the proxy; do not keep the original in a variable. |
8. A value read after an await inside watchEffect | Tracking only runs while the function runs synchronously. Anything read after the first await is not a dependency. | Read every source before the first await, or use watch with explicit sources. |
Computed or watch: a value or an action
This is the decision Vue developers get wrong most often, and it has a clean rule. A computed derives a value: it is cached, it recalculates only when something it read has changed, and it must have no side effects. A watch reacts to a change by doing something — calling an API, writing to storage, starting an animation. The test is one question: are you producing a value or performing an action?
A watcher that ends by assigning to other state is almost always a computed written the long way, and the usual explanation — “it runs a tick late” — is close but not quite the mechanism. A computed is lazy: writing one of its sources only marks it stale, and the next read recalculates it on the spot, so the very next line of code already sees the new value. A watcher’s callback is queued instead. By default it runs after your current code finishes and before Vue updates the component’s DOMDocument Object Model — the page as a tree of objectsThe tree of objects a browser turns an HTML page into, so that code can read and change it. Change the DOM and the reader sees the change immediately; the HTML file the server sent stays exactly as it was.Full article →All terms. So the screen is never wrong — the render waits for the queue — but every line of script between the write and the queue reads the old value, and before the first change the copy holds nothing at all unless you add immediate: true.
Those costs add up: a second piece of state that can disagree with the first, a value that is empty on the first render, and a loop on the day two watchers write to each other’s sources. In development Vue stops that loop after a hundred recursive updates and warns “Maximum recursive updates exceeded” — which is the moment most teams discover they had a computed all along. When a watcher is right, the one choice left is when it runs:
| flush | When the callback runs | Use it for |
|---|---|---|
| 'pre' (default) | Queued; runs before the component’s DOM is updated, so the DOM it sees is the old one | Almost everything: requests, storage, logging |
| 'post' | Queued; runs after the DOM is updated (watchPostEffect is the shorthand) | Measuring an element, moving focus, handing the DOM to a third-party library |
| 'sync' | Immediately on every write, with no batching: ten writes, ten runs | Rarely anything in application code |
Templates the compiler can read
A .vue file holds three blocks: the template, the script and the styles. The argument for keeping them in one file is that they change together — editing a component means editing all three. The styles block takes a scoped attribute, and it is a genuinely useful default: Vue adds a generated attribute to the component’s elements and limits every selector to it, so a rule cannot leak into another component. That is the locality CSS methodologies are usually adopted to provide, supplied by the compiler.
The template itself is HTML with directives — attributes starting with v- that Vue compiles into code. Six of them carry most of the work, and each has one trap worth knowing before it bites:
| Directive | What it does | The trap |
|---|---|---|
| v-if | Adds or removes the element; components inside are created and destroyed each time | A heavy panel toggled often — every toggle builds it again |
| v-show | Keeps the element and toggles its CSS display | Something expensive that is rarely shown — it is rendered on the first load anyway |
| v-for + :key | Renders a list; the key is each item’s identity between renders | The index as the key: state and focus stay with the position when the data moves |
| v-if with v-for | On one element, Vue 3 evaluates v-if first, so the condition cannot see the loop variable | Code written for Vue 2, where the order was the reverse — filter in a computed instead |
| v-model | Two-way binding: a value goes down, an update event comes back up | An object passed through it and changed in place — the parent changes without the event |
| v-html | Inserts a string as raw HTML, with no escaping at all | Anything a user wrote: it is the one door to cross-site scripting that Vue leaves open |
The template has a second job, and it is the reason Vue kept templates when much of the front end moved to JSXmarkup written inside codeA syntax that lets you write markup directly in JavaScript. It is not HTML embedded in code but a shorthand: before the code runs, every tag is turned into an ordinary function call.Full article →All terms. Like React, Vue renders through a virtual DOM — a lightweight copy of the page kept in JavaScript: on each update it builds a new copy, compares it with the previous one, and changes only the real DOM nodes that differ. Comparing is the cost. A template is a restricted language, so the compiler can read it and know, before the page ever runs, which parts can change and which cannot. Vue calls the result a compiler-informed virtual DOM. Static parts are created once and reused. Each dynamic node carries a patch flag saying what about it can change — its text, its class, one attribute. And the dynamic nodes inside a block are collected into one flat list, so an update visits them directly instead of walking the whole tree.
Vapor Mode takes the same idea to its end. It compiles a component straight into DOM operations, with no virtual DOM at all, and in third-party benchmarks it reaches the same level as Solid and Svelte 5. It is part of Vue 3.6, which has been a release candidate since July 2026 and is not yet stable. It is opt-in per component, with <script setup vapor>; it supports the Composition API only, not the Options API; and the Vue team recommends it for now for a single performance-sensitive page in an existing app, or for a small new app built entirely in it.
Composition API, Options API and composables
Vue has two ways to write a component’s script, and both are supported. The Options API, which Vue started with, groups code by kind: data, computed, methods, watch. The Composition API, added in Vue 3, lets you group it by feature, as the functions in the table at the top of the page.
Composition API
- Code grouped by feature, not by kind of option
- Logic moves out into plain functions — composables
- Types infer on their own, so TypeScriptJavaScript with type checkingJavaScript plus a description of what each function takes and returns, checked in the editor before anything runs. It never reaches the browser — it is compiled back into plain JavaScript first, so the types are a development-time tool and nothing more.Full article →All terms needs no help
- The default for new code, and the only one Vapor Mode compiles
Options API
- Code grouped into data, computed, methods, watch
- Reuse through mixins, whose origin is hard to trace
- An easier first hour; a harder tenth component
- Supported, and common in existing code
The difference shows at size rather than at the start. In a component with three concerns, the Options API spreads each of them across four sections, so understanding “how does search work here” means reading four places and holding them together. The Composition API lets the three concerns sit as three blocks — and lets any of them move out into a composable that another component imports. A composable is simply a function whose name starts with use and which calls Vue’s reactivity functions inside: useSearch(), useMouse(), useFetch(url).
Composables replaced mixins, and Vue’s documentation names the three reasons. With mixins it was unclear which mixin a property came from; two mixins could collide on the same name; and mixins talked to each other through shared properties nobody declared. A composable has none of those problems, because it is an ordinary function call: what it returns is visible where you call it, and you can rename it there.
Vue 2 to Vue 3, and what comes in the box
Vue 3 was released in September 2020, and it was not an upgrade in the usual sense. The reactivity system was rebuilt on proxies, several Vue 2 features were removed, and the official tools around the framework were replaced one by one. A great deal of material online still describes Vue 2, so it is worth knowing which half of a tutorial you are reading:
| Job | Vue 2 | Vue 3 |
|---|---|---|
| Tracking changes | Object.defineProperty: cannot see an added property or an array item set by index; Vue.set covers the gap | Proxy: sees both, and Vue.set is gone |
| Writing a component | Options API | Composition API with <script setup>; Options API still supported |
| Shared state | Vuex | Pinia — Vuex is in maintenance mode |
| Starting a project | Vue CLI, on webpack | npm create vue, on Vite |
| Editor support | Vetur | Vue – Official (formerly Volar) |
| Events between unrelated components | An event bus on $on, $off, $once | Removed; props and events, a store, or a small emitter library |
| Formatting in templates | Filters: {{ price | currency }} | Removed; a function or a computed |
| Browsers | Including Internet Explorer 11 | Modern browsers only: a proxy cannot be imitated in older JavaScript |
Around the core, Vue sits between React and Angular in how much it decides for you. It is not a library that leaves everything open, and not a framework that supplies the whole architecture. It has a small set of official packages, and because they are official, the ecosystem gathers around them rather than splintering: Vue Router for routing; Pinia for shared state, small enough that using it for one shared value is not overkill; Nuxt as the framework for server rendering, routing conventions and deployment; and Vitea front-end build toolA build tool and development server for front-end code, started in 2020 by Vue’s author and now used with most frameworks. In development it serves your source files to the browser almost as they are, so it starts in a moment; for production it bundles and minifies them.All terms, the build tool Vue’s author started in 2020 and which much of the JavaScript world now uses whatever its framework. The reactivity system is even published on its own, as @vue/reactivity, and works with no components at all — a fair signal of where the framework’s centre of gravity is.
Vue against React
| Question | Vue | React |
|---|---|---|
| How are dependencies known? | Seen as they are read | Listed by you, checked by a lint rule |
| What runs again on a change? | Only the effects that read the value | The component and everything below it |
| How is the view written? | A template the compiler analyses | JSX — ordinary JavaScript |
| Who decides the architecture? | Official router and store | You do, or the framework you pick |
| Where does a value get lost? | Copied out of the proxy | Changed in place instead of replaced |
Neither column is a verdict. The template lets the compiler know more, so Vue can skip at run time work React has to do — though React Compiler now closes part of that gap from the other side. JSX is just JavaScript, so anything the language can express, the view can express. Choose on the two things that really differ for a team: whether you want conventions supplied, and which pool of experienced developers you can hire from.
Five cases, and what each one was missing
Each case below is documented by the Vue team or by the company it happened to. Four are about Vue’s own history, because the framework’s biggest lessons were learned in public, and one is about what that history costs a large application that has to live through it.
| Case | What happened | What was missing |
|---|---|---|
| Vue 2’s blind spots, 2016–2020 | A property added later, or an array item set by index, did not update the screen; Vue.set was the workaround | A tracking mechanism that could see every write — the reason for proxies in Vue 3 |
| Vue 3 as the default, Feb 2022 | The core shipped in September 2020, but became the default only seventeen months later | A router, a store, tooling and devtools ready on the same day as the core |
| Reactivity Transform, 2023 | An experimental syntax that removed .value was withdrawn; teams that adopted it had to migrate back | A rule against experimental compiler features in production code |
| Vue 2 end of life, Dec 2023 | On 31 December 2023 Vue 2 stopped receiving fixes of any kind, security fixes included | A migration scheduled before the date rather than after it |
| GitLab, 2026 | Nearly three years after that date, most of GitLab’s 579 front-end entry points still run on Vue 2 | A code base free of the patterns the next major version removes |
The first case is where Vue 3’s design comes from. Vue 2 made data reactive by converting each existing property into a getter and a setter with Object.defineProperty, the only tool JavaScript had in 2014 that could run everywhere. The tool could only convert properties that already existed. A property added after the object was made reactive, or an array item assigned by index, went around the setters, and the screen did not change. The Vue 2 guide listed these cases under “Change Detection Caveats”, and Vue.set existed to work around them. Vue 3 moved to proxies, which intercept every read and write, including ones to properties that did not exist yet. The price was Internet Explorer 11: a proxy cannot be imitated in older JavaScript, so Vue 3 does not run there.
The second is a lesson about what a release is. Vue 3.0 came out on 18 September 2020, and for seventeen months npm install vue still gave you Vue 2. The Vue team explained why when it finally switched, on 7 February 2022: “When Vue 3 core was ready, most other parts of the framework were either in beta or still awaiting update.” A team that moved early had a faster core and no stable router, store, editor support or devtools to go with it. The switch came when Vite, Pinia, Volar and the new devtools were ready together.
The fourth case has a date on it. Vue 2 reached end of life on 31 December 2023, and the official page says what that means without softening it: Vue 2 “no longer receives new features, updates, or fixes”. Version 2.7.16 is the last one there will ever be. The code keeps working and stays on every CDN and package registry, so nothing breaks on 1 January — which is precisely the danger. The next vulnerability found in Vue 2 has no official fix, and the page’s own advice to organisations that cannot move is to buy extended support from a third party and to tell their customers and their security officer what the plan is.
Where it degrades
Vue’s reputation for being approachable is earned and slightly misleading. The first day really is easier: a template looks like HTML, reactivity does what you expect, and there is no dependency array to get wrong. The part that needs care arrives later and is specific — knowing exactly where a value stays tracked and where it is copied out — because every failure in that part is silent. These are the shapes it takes.
- Destructuring or copying a tracked value, which silently detaches it — rows 1 to 5.
- A watcher that assigns to other state: a
computedthat is empty at first and stale in between. - Watchers that fetch without
onWatcherCleanup, so a slow old answer overwrites a fast new one. - Deeply reactive large data, where the proxy work is real and
shallowRefwas the answer. - Mutating a prop, or an object received through
v-model, which breaks the one-way flow. provide/injectused as a global variable, which hides where a value comes from.v-htmlon anything a user wrote.- Experimental syntax in production code, which the next minor version is allowed to remove.
- A code base still on Vue 2, which has had no fixes of any kind since 31 December 2023.
There is also an honest limit, built into the mechanism the page opened with. Vue guarantees that what reads a value re-runs when the value changes. It says nothing about whether the value is right, and it cannot track what it does not own: state that lives in a third-party library, a Web API or a closure is invisible until you bring it inside a ref. The hiring pool is the other limit, and it is not a technical one — in many markets it is smaller than React’s, which matters more than any benchmark when the team will change hands.
The practical check takes a minute. Open Vue DevTools and look at what re-rendered after the one action the screen exists for. When something updates that should not, or fails to update when it should, add onRenderTriggered to the component: in development it tells you exactly which read of which property started each render. A value that never appears there was never tracked — and the table in section two says why.
When to use it
Use it when
- You want official answers for routing, state and the build rather than a comparison exercise.
- The team includes people who write more markup than JavaScript — templates fit that shape.
- You want updates that reach only what changed, without tuning what re-renders by hand.
- Scoped styles by default would replace a CSS methodology you would otherwise have to adopt.
Avoid it when
- The local hiring market is overwhelmingly React and the team will change hands.
- The project depends on libraries that exist today only for React.
- A page with almost no state — the reactive machinery is overhead you would never use.
- Nobody will learn where a value stops being tracked, which makes the silent failures expensive.
Sources and method
What this page stands on
- 12 sources
- Checked October 13, 2026
Practices and where the terms come from
the Composition API on one screen, reactivity, watchers and the versions- DocumentationReactivity Fundamentals (opens in a new tab)vuejs.org
- DocumentationReactivity in Depth (opens in a new tab)vuejs.org
- DocumentationWatchers (opens in a new tab)vuejs.org
- ArticleAnnouncing Vue 3.4 (opens in a new tab)The Vue Point · Dec 28, 2023
- ArticleAnnouncing Vue 3.5 (opens in a new tab)The Vue Point · Sep 1, 2024
- Documentationvuejs/core — CHANGELOG (opens in a new tab)GitHub
The cases
the section “Five cases, and what each one was missing”- DocumentationVue 2 — Reactivity in Depth: Change Detection Caveats (opens in a new tab)v2.vuejs.org
- ArticleVue 3 as the New Default (opens in a new tab)The Vue Point · Jan 20, 2022
- ArticleReactivity Transform — vuejs/rfcs discussion #369 (opens in a new tab)GitHub · Feb 21, 2023
- DocumentationVue 2 End of Life (opens in a new tab)v2.vuejs.org · Dec 31, 2023
- DocumentationMigration to Vue 3 (opens in a new tab)GitLab Docs
- DocumentationMigration from Vue 2 to Vue 3 (&6252) (opens in a new tab)GitLab.org
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.