← All posts

Vue 3: reactivity and Composition API pitfalls

ref vs reactive, destructuring traps, watchEffect loops, Teleport context — common Vue 3 mistakes from Dev.to.

Contents

In brief

Vue 3 with Proxy reactivity, Composition API, and TypeScript looks simpler than Vue 2 — until production teaches the difference between “can use” and “use correctly.” A Dev.to roundup covers reactivity traps, ref vs reactive, watchers, and newer pieces like Teleport and Suspense.

What happened

Reactivity moved from Object.defineProperty to Proxy, fixing Vue 2 pain points (new object keys, array indices). The trade-off is a new mental model: where does the DOM update link break when you destructure or replace whole objects?

Rule of thumb from the article: ref for primitives and objects you might replace entirely; reactive for nested structures like forms. The trap: reactive returns a proxy — let { count } = state gives a plain number; incrementing it won’t re-render. Fix with toRefs so each field stays a ref wrapper.

Ref unwrapping differs by container: template auto-unwraps, reactive arrays unwrap refs inside them, plain arrays do not — you need .value. Replacing a whole reactive object (data = { items: [...] }) drops reactivity; use ref + data.value = … or Object.assign on the existing proxy.

In Composition API, watchEffect runs immediately and tracks deps — great for logging, dangerous if you mutate those same refs (infinite loop). For controlled old/new comparisons on a specific source, prefer watch.

Why it matters

Teams pick Vue 3 as “lighter React,” but many production bugs are broken reactivity chains: composables returning .value instead of refs, lifecycle hooks registered inside setTimeout, scoped styles missing Teleport content. Linters rarely catch these; SSR exposes them late.

You need an explicit composable contract — expose refs/reactive, not bare values — and review destructuring. TypeScript helps only with disciplined defineProps<{…}>() + withDefaults, not mixed runtime/type declarations.

In practice

  1. Destructuring reactive? Wrap with toRefs or lose reactivity.
  2. Data may be fully replaced? Use ref, not reactive.
  3. Don’t mutate deps blindly inside watchEffect; use watch for old/new.
  4. Composables return { x, y } as refs, not unwrapped values.
  5. Register onMounted etc. synchronously in setup, not from async callbacks.
  6. Teleport moves DOM, not component context — use :deep() or global CSS.
  7. Treat Suspense + async components carefully in SSR and critical routes.

Takeaway

Vue 3’s difficulty isn’t syntax — it’s the reactivity model. The Dev.to post is a solid checklist before migrating to Composition API: ref/reactive, watchers, composable contracts, Teleport edge cases. See the original for code samples.