As a Vue 3 lead, would you standardise a codebase on ref() only, and what does a ref-only convention cost?
answer
- one rule versus ergonomics
- replacement is always safe
- the truthy wrapper bug
- proxies still exist underneath
basics
~20 sUsually yes: ref-only gives one declaration rule that survives replacement and passing values around. It costs .value noise, forgotten-.value bugs in script, heavier grouped form state, and it does not remove proxies, since object values still go through reactive().
solid answer
~50 sA ref-only convention means every piece of Composition API state is declared with `ref()`, and `reactive()` appears only in documented exceptions. It buys one rule for primitives and objects alike, safe whole-value replacement via `.value = next`, and state that stays live when handed to functions and composables - exactly the limitations the Vue docs cite against `reactive()`. The costs are real: `.value` everywhere in script, bugs when it is forgotten (a ref object is always truthy, so `if (isOpen)` always runs), grouped form state that reads as `form.value.email`, and a false sense that proxies are gone - a ref holding an object still converts it with `reactive()`, so identity and deep-conversion behaviour are unchanged. I would adopt it, allow `reactive()` for fixed-shape local objects that are never replaced, and enforce the rule in review rather than by taste.
code
ts · 13 linesimport { ref } from 'vue'
const isOpen = ref(false)
// Bug under a ref-only rule: a ref object is always truthy
if (isOpen) {
console.log('always runs')
}
// Correct
if (isOpen.value) {
console.log('runs only when open')
}go deeper
Know that the Vue docs recommend ref() as the primary API and that in script you always need .value, since only templates unwrap.
Be able to list what ref-only prevents (lost connection on replacement and on passing values) and the forgotten-.value bugs it introduces.
Point to real bugs on both sides and propose where reactive() is still fine, such as a fixed-shape local form never reassigned.
Decide from the team's bug history and type safety, document the exceptions with their reason, enforce mechanically, and migrate opportunistically rather than by a mass rewrite.
## What the convention actually says A **ref-only convention** is a team rule for Vue 3 Composition API code: every piece of reactive state is declared with `ref()` (or a ref-returning helper), and `reactive()` is either banned or allowed only in named exceptions. It is a judgment call, not a framework requirement - `reactive()` is a supported API - but the Vue guide itself recommends `ref()` as the primary API, which gives the convention a strong starting point. ## What it buys - **One rule for every value.** Primitives, objects, arrays and nullable values are all declared the same way; nobody has to decide per variable. - **Safe replacement.** `state.value = next` keeps every subscriber. The classic `reactive()` bug - reassigning the variable and losing the reactive connection - cannot be written. - **Values that travel.** A ref handed to a function, a composable or a child keeps its live value. With `reactive()`, passing out a primitive property hands over a dead copy. - **Visible reactivity.** In script, `.value` marks every reactive read and write, which makes reviews of effects and async code easier to reason about. - **Uniform composable shapes.** When every composable returns refs, callers learn one consumption pattern. ## What it costs 1. **`.value` noise.** Script code grows longer, especially for grouped state: `form.value.email` instead of `form.email`. 2. **Forgotten `.value` bugs.** A ref is an object, so it is always truthy: `if (isOpen) { ... }` runs even when `isOpen.value` is `false`. `count + 1` concatenates to `[object Object]1`. Comparing two refs with `===` compares wrappers, not values. Type checking catches some of these, such as arithmetic on a ref or passing a ref where a string is expected, but not the always-truthy condition. 3. **Template versus script asymmetry.** Templates unwrap top-level refs, so the same variable is written two ways, and newcomers mix them up. 4. **No escape from proxies.** A ref whose value is an object converts that object with `reactive()`. Deep conversion, proxy identity and `toRaw` concerns are all still present; ref-only changes how you *access and replace* state, not whether proxies exist. Teams that adopt the rule hoping to avoid proxy behaviour are disappointed. ## Where exceptions are reasonable | Situation | Ref-only | Allow `reactive()`? | |---|---|---| | Single primitive or nullable value | `ref()` | no | | State replaced after a fetch or a reset | `ref()` | no - replacement is the failure mode | | Fixed-shape local form mutated field by field | `ref()` works | yes, if never replaced | | Object shared across many composables | `ref()` | rarely - replacement tends to appear later | | Existing code that already uses `reactive()` correctly | migrate opportunistically | yes, until touched | The honest middle ground many teams land on is **"ref by default, reactive for local fixed-shape objects"**, with the exception stated in writing so it is not reopened in every review. ## How a lead decides and enforces it 1. **Look at the bugs you already have.** If reassigned `reactive()` variables or dead primitives passed to helpers appear in incident notes or review comments, the rule pays for itself. 2. **Weigh the team's type safety.** With typed code part of the forgotten-`.value` cost is caught by the compiler; in untyped code all of it is paid at runtime, which shifts the balance. 3. **Write the rule and its exceptions down** in the contribution guide, with the reason - the docs' three `reactive()` limitations - so the convention survives turnover. 4. **Enforce it mechanically where possible** - a search for `let` bindings of `reactive(`, a review checklist item, a lint rule if your toolchain offers one - rather than by taste. 5. **Do not mass-rewrite.** Converting working `reactive()` code touches every access site (`state.x` becomes `state.value.x`); migrate when a file is changed for another reason. ## The judgment There is no universally right answer. For most application teams, ref-only (with a narrow exception) trades a small, constant syntax cost for the removal of a whole bug class and one less decision per variable. For a small team with a lot of form-heavy code and disciplined reviewers, a mixed style can be equally sound - provided everyone knows that anything which might be replaced must be a ref.
- Does a ref-only rule in Vue 3 mean the codebase no longer creates reactive proxies?No. When a ref's value is an object, `ref()` converts it with `reactive()`, so `form.value` is a deep proxy. Everything that follows from proxies - comparing a proxy with its raw object, deep conversion cost - still applies. The rule changes access and replacement, not the underlying mechanism.
- Which reactive() use would you still allow under a Vue 3 ref-by-default rule, and why?A local, fixed-shape object that is only mutated field by field and never reassigned, such as a small form model, because none of the documented limitations bite there and `form.email` reads better than `form.value.email`. The exception should be written down so it does not grow into state that later needs replacing.
saying these in an interview costs you the question
- A ref-only rule removes all Proxy behaviour from the codebase.
- Forgetting .value in script is harmless because Vue unwraps refs everywhere.
- reactive() should be banned because it is deprecated in Vue 3.5.
- Existing reactive() code must be rewritten immediately once the rule is adopted.
- The choice between ref and reactive has no effect on bug rates, only on style.