understanding the experiment
Reactivity Transform promised reactive values without .value, destructurable directly like normal variables. The compiler experiment was officially dropped in 2023. If you read old code or old blog posts about it today, you should understand how it worked, why it failed, and what remains relevant for practice.
Table of Contents
- 1. What Reactivity Transform was meant to solve
- 2. $ref, $computed and the macro syntax in detail
- 3. Destructuring reactive objects without .value
- 4. A practical example: a composable before and after
- 5. Why the experiment was dropped in 2023
- 6. Build tooling and compiler requirements
- 7. Migrating away from Reactivity Transform code
- 8. Lessons for today's Vue 3 practice
- 9. Reactivity Transform compared to the standard API
- 10. Summary
- 11. FAQ
1. What Reactivity Transform was meant to solve
Reactivity Transform was an experimental compiler feature in Vue 3 that existed as an optional opt in between version 3.0 and 3.4. The core idea: instead of writing .value explicitly on every access to a ref, a compiler macro such as $ref() would let you read and write reactive values like normal variables. Reactivity Transform tried to eliminate the most common source of beginner confusion this way, namely constantly forgetting .value in script logic outside of templates.
The compiler analyzed the source code at build time and replaced the macro calls with actual ref() calls, with .value access inserted automatically. To the developer, the code looked as if the .value indirection did not exist at all, while at runtime exactly the same reactivity system ran as without Reactivity Transform. The goal was purely syntactic, not a new reactivity engine, just more compact code for the same semantics.
Important for understanding this: Reactivity Transform was never part of the official recommendation for new projects, it was always marked as experimental, similar to a research prototype within the Vue ecosystem. Anyone who runs into older tutorials, GitHub repos, or Stack Overflow answers from 2021 to 2023 today frequently finds Reactivity Transform syntax, without it being obvious that the feature has since been removed from core.
2. $ref, $computed and the macro syntax in detail
The central macro function was $ref(), a compiler macro that looked like a normal function call on the surface, but at build time was treated completely differently from a real JavaScript function. let count = $ref(0) was transformed by the compiler into let count = ref(0), with every later read of count automatically becoming count.value and every assignment automatically becoming count.value = .... Similarly there was $computed() for computed values, and $$() to convert an already transformed value back into a real ref when it had to be passed to a function expecting an actual ref.
These macros only worked because a special compiler pass analyzed the entire <script setup> block and rewrote every usage of the marked variables throughout the entire function body. That is fundamentally different from a normal function executed at runtime. Reactivity Transform was therefore inseparably tied to Vue's specific single file component compiler and could not work in plain .js files without that build step.
// Reactivity Transform syntax (experimental, removed in Vue 3.4)
// Requires the compiler macro to be enabled explicitly in build config
let count = $ref(0)
let doubled = $computed(() => count * 2)
function increment() {
count++ // compiled to count.value++
}
// Passing a transformed variable to a function expecting a real ref
function useExternalRef(actualRef) {
console.log(actualRef.value)
}
useExternalRef($$(count)) // $$() converts back to a real ref
3. Destructuring reactive objects without .value
A second central promise of Reactivity Transform was $(), a macro that allowed destructuring reactive objects without losing reactivity. Normally, a destructuring assignment like const { x, y } = reactive({ x: 1, y: 2 }) breaks the connection to the reactive object, because x and y become plain, non reactive copies. With $(), the compiler transformed each destructured variable into its own ref, still coupled to the original.
This pattern was especially attractive for composables that often receive a reactive options object and want to destructure individual fields out of it, without manually calling toRef() for each field. Reactivity Transform promised a significant reduction in boilerplate here, since toRef() calls for every single destructured field became unnecessary.
// Reactivity Transform: destructuring without losing reactivity
import { reactive } from 'vue'
function useMousePosition() {
const state = reactive({ x: 0, y: 0 })
window.addEventListener('mousemove', (e) => {
state.x = e.clientX
state.y = e.clientY
})
return $(state) // marks destructured fields as still reactive
}
// In a component:
let { x, y } = useMousePosition()
// x and y stay reactive despite destructuring, thanks to the $() macro
4. A practical example: a composable before and after
To make the actual difference tangible, a direct before and after comparison of the same composable helps. Without Reactivity Transform, every access to a ref's value inside the logic carries .value, which quickly becomes cluttered in composables with many internal computations. With Reactivity Transform, that indirection disappears entirely from visible code, while the compiled result stays exactly the same.
The difference is purely cosmetic and affects only readability during development. For runtime behavior, performance, and dependency tracking, it makes no difference whether .value is visible in the source code or inserted automatically by the compiler. That purely syntactic benefit later became the decisive point of criticism that led to Reactivity Transform being dropped.
// WITHOUT Reactivity Transform: standard Composition API
import { ref, computed } from 'vue'
function useCounter(initial = 0) {
const count = ref(initial)
const isEven = computed(() => count.value % 2 === 0)
function increment() {
count.value++
}
function reset() {
count.value = initial
}
return { count, isEven, increment, reset }
}
// WITH Reactivity Transform (removed feature, shown for comparison only)
// function useCounter(initial = 0) {
// let count = $ref(initial)
// const isEven = $computed(() => count % 2 === 0)
// function increment() { count++ }
// function reset() { count = initial }
// return { count: $$(count), isEven, increment, reset }
// }
5. Why the experiment was dropped in 2023
The Vue core team officially withdrew Reactivity Transform from the standard recommendations in mid 2023 and removed it from core as of Vue 3.4, even though it remains usable via plugins. The main reason was a fundamental tension between the promised simplification and the actual cognitive load: code with $ref() looked simpler locally, but became harder to understand once a variable was passed between multiple functions or files, because it was no longer obvious at a glance whether a value was reactive or not.
Another reason was IDE support. Because $ref() was not a real function call but a pure compiler macro, tools like Volar had to implement special case handling to support type inference, autocomplete, and refactoring correctly. That special handling was fragile and led to inconsistencies between different editor versions. The Vue team concluded that the explicit .value syntax, despite its verbosity, is the more reliable foundation for tooling and team readability.
6. Build tooling and compiler requirements
For Reactivity Transform to work at all, the feature had to be explicitly enabled in the build setup, both in Vite and in the Vue loader for webpack projects. Without that explicit activation, the compiler ignored the macros completely, and $ref() would simply have been an undefined function at runtime, causing an immediate runtime error.
This dependency on a specific build configuration made Reactivity Transform unsuitable from the start for libraries consumed by different users with different build setups. An npm library that internally used $ref() syntax would either have had to ship precompiled code, or require consumers to enable the same compiler option, a compatibility risk that many library authors avoided from the outset.
// vite.config.js — how Reactivity Transform had to be enabled (historical)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
reactivityTransform: true // required explicit opt-in, removed in Vue 3.4
})
]
})
7. Migrating away from Reactivity Transform code
Anyone who still encounters an old project using Reactivity Transform syntax today needs to migrate it before any Vue 3.4 upgrade, since the macros stop working from that version onward without a separate community plugin. The migration is mechanical but not trivial: every $ref() becomes ref(), and every read access to the affected variable needs .value added, which is easy to miss without tool support, especially for destructured values from $().
The official Vue team provided a migration codemod for this transition that could automatically rewrite most cases. For complex code with nested destructuring or transformed variables being passed to external functions, however, manual follow up work was necessary, since the codemod could not always safely distinguish between a reactive and a normal variable.
// BEFORE migration: Reactivity Transform syntax
// let price = $ref(19.99)
// let total = $computed(() => price * quantity)
// AFTER migration: standard Composition API
import { ref, computed } from 'vue'
const price = ref(19.99)
const quantity = ref(1)
const total = computed(() => price.value * quantity.value)
function applyDiscount(percent) {
price.value = price.value * (1 - percent / 100)
}
8. Lessons for today's Vue 3 practice
Even though Reactivity Transform is no longer actively developed, the experiment offers concrete lessons for today's day to day Vue 3 work. The most important one: syntactic sugar that makes reactivity invisible feels convenient locally, but shifts the cost to later refactorings and to teammates who have to read the code without the full context. Explicit .value is a deliberate trade off in favor of traceability, not a design flaw to work around.
A second lesson concerns experimental compiler features in general: anyone adopting them in production code should be aware of reversibility. Reactivity Transform was clearly labeled experimental by Vue itself, yet some teams adopted the syntax extensively, which made the later migration more work than necessary. The same caution applies to current experimental Vue features: test them first in small, isolated modules before letting them spread through the entire codebase.
Third, the history of Reactivity Transform shows how heavily Vue weighs IDE experience as a design criterion. A feature that degrades editor ergonomics gets withdrawn even if it looks more elegant in raw source code. This prioritization of reliable tooling over cosmetic compactness is a recurring pattern in Vue 3's evolution, and a good benchmark for your own architecture decisions in composables.
9. Reactivity Transform compared to the standard API
Even though Reactivity Transform is no longer recommended for new projects today, a direct comparison helps to understand the concrete trade offs that led to it being dropped.
| Criterion | Reactivity Transform | Standard Composition API | Outcome |
|---|---|---|---|
| Local readability | No .value needed | .value on every access | Transform more compact, but deceptive |
| Discoverability of reactive values | Not obvious at a glance | .value makes reactivity explicit | Standard API wins for team work |
| IDE support | Required fragile special casing | Normal TypeScript inference | Standard API more reliable |
| Library compatibility | Build setup dependent | Works everywhere | No compatibility risk with standard API |
| Current status | Removed from core since 3.4 | Officially recommended | Clear choice for new projects |
The table shows why the Vue team weighed the short term convenience of Reactivity Transform against the long term clarity of the explicit .value syntax and chose the latter. For new Vue 3 projects, the rule has been unambiguous ever since: standard ref() and reactive() without compiler macros, complemented by toRef() or toRefs() for destructuring cases.
Mironsoft
Vue 3 legacy code migration and modernization
Still have Reactivity Transform syntax in your code?
We cleanly migrate existing Vue 3 projects with outdated $ref syntax to the standard Composition API and prepare your codebase for future Vue versions.
Code audit
Identifying outdated compiler macros and compatibility risks
Migration
Cleanly moving $ref, $computed and $() to the standard API
Team training
Teaching current best practices for Vue 3 reactivity
10. Summary
Reactivity Transform was an ambitious compiler experiment promising reactive values without a visible .value, with $ref(), $computed(), and $() as its central macro building blocks. The idea looked convincing locally, but failed due to lack of discoverability of reactive values across function boundaries, fragile IDE support, and insufficient library compatibility. The Vue team officially withdrew Reactivity Transform in 2023 and removed it from core as of Vue 3.4.
For today's projects, the lesson stands: explicit .value access is not unnecessary baggage, but a deliberate trade off in favor of clarity, tooling reliability, and portability across build setups. Anyone encountering old code with Reactivity Transform syntax should consistently migrate it to standard ref() and computed() before any major upgrade.
Reactivity Transform in Vue 3 — The Essentials at a Glance
What it was
Compiler macros $ref(), $computed(), $() for refs without visible .value.
Current status
Withdrawn in 2023, removed from core as of Vue 3.4, only available via a community plugin.
Main reason for dropping it
Reactivity no longer discoverable across function boundaries, fragile IDE support.
Recommendation today
Use standard ref()/computed(), migrate old code with the codemod.