Because reactive() is built on JavaScript Proxy objects, and Proxies can only wrap objects. Primitive values (strings, numbers, booleans) are not objects, so there is nothing for the Proxy to wrap.
ts
const count = reactive(0) // ⚠️ value cannot be made reactive: 0
const name = reactive('Vue') // ⚠️ value cannot be made reactive: Vue
const active = reactive(true) // ⚠️ value cannot be made reactive: trueVue will log a warning and return the raw value without any reactivity. Your template won't update when these values change.
Use ref() instead
ref() was designed precisely for this. It wraps any value (including primitives) inside an object with a .value property, which Vue can track.
ts
const count = ref(0) // works
const name = ref('Vue') // works
const active = ref(true) // works
count.value++ // reactive, triggers updatesWhen to use each
ref() | reactive() | |
|---|---|---|
| Primitives | Yes | No |
| Objects | Yes (wraps in .value) | Yes (direct access) |
| Reassignable | Yes (count.value = newVal) | No (loses proxy) |
Needs .value | In script, not in template | Never |
Most teams just use ref() for everything. It handles both primitives and objects, and the consistency avoids this kind of mistake entirely.
See also: What is the difference between ref and reactive? · Why do I lose reactivity when destructuring a reactive object?