| |

Vue.js 89 🟢 Raw Object Escaping with markRaw() and toRaw()

⏱️ Estimated read: 11 min | |

Vue’s reactivity system wraps objects in Proxies to track property access and mutation. This is the mechanism that makes the UI reactive, but it is also a mechanism that some objects cannot tolerate. A third-party library instance that performs identity checks, a DOM node that the browser expects to be the exact object it created, or a component definition that Vue itself warns about—all of these break when wrapped in a Proxy. markRaw() and toRaw() are the two escape hatches. markRaw() marks an object so it is never converted into a Proxy, even when nested inside reactive state. toRaw() retrieves the original object from an existing Proxy, which is useful when the object has already been wrapped and needs to be passed to code that cannot handle a Proxy .

Key point: markRaw() prevents future conversion; toRaw() retrieves the original from an existing conversion. The first is used at the point of creation or assignment. The second is used at the point of consumption, when the object has already been made reactive and needs to be unwrapped for an external API.


Why raw object escaping exists

The identity problem. A Proxy is not the same object as its target. When a library stores a reference to an instance and later compares it to the object passed into a function, the comparison fails if one side is a Proxy and the other is the raw target. CodeMirror’s setValue method, for example, broke when the editor instance was wrapped in a Vue Proxy because the library’s internal identity check failed . markRaw() prevents the wrapping, so the identity is preserved.

The this binding problem. A class with private fields or methods that rely on this being the exact instance can misbehave when accessed through a Proxy. The Proxy’s get trap returns a value, but the this inside the method is the Proxy, not the raw instance. markRaw() ensures the class instance is never proxied, so this remains correct .

The component warning problem. Storing a Vue component definition in ref() or reactive() triggers a warning: “Vue received a Component that was made a reactive object.” The component’s internal properties should not be reactive. markRaw() on the component, or shallowRef() for the container, suppresses the warning and avoids the unnecessary overhead .

The API boundary problem. A reactive object cannot be passed directly to a backend API or a non-Vue library that expects a plain JavaScript object. The Proxy’s traps will interfere with serialization, or the library’s identity checks will fail. toRaw() retrieves the underlying object, which is the original plain object that can be serialized or passed safely .


a. markRaw: Preventing Conversion

markRaw() takes an object and marks it with a non-enumerable property that tells Vue’s reactivity system to skip it. The function returns the object itself, so it can be used inline .

import { markRaw, reactive, isReactive } from 'vue'

const rawObject = markRaw({ value: 1 })
const state = reactive({ data: rawObject })

console.log(isReactive(state.data)) // false

The mark is permanent for the object’s lifetime. Once marked, the object will not be converted by reactive(), shallowReactive(), or any other deep conversion, even when nested inside a reactive structure .

The common use cases are library instances, DOM nodes, class instances with internal state, and large static data:

import { markRaw, reactive } from 'vue'
import mapboxgl from 'mapbox-gl'

const state = reactive({
  map: markRaw(new mapboxgl.Map({ container: 'map' })),
})

A limitation is that markRaw only affects the root level. A nested object inside a markRaw object may still be proxied if accessed through a reactive parent. The safer container for objects that should never be reactive is shallowRef with markRaw .


b. toRaw: Retrieving the Original

toRaw() takes a reactive or readonly Proxy and returns the underlying target object. The returned object is not reactive, and mutations to it do not trigger updates .

import { reactive, toRaw } from 'vue'

const state = reactive({ count: 0 })
const raw = toRaw(state)

raw.count = 10
console.log(state.count) // 0 (the Proxy was not modified)

The function is an escape hatch for temporary reads or writes without proxy overhead. The Vue documentation warns that it is “not recommended to hold a persistent reference to the original object” because the raw object and the Proxy can diverge, and the reactivity is lost for the raw reference .

The common use case is passing data to an external API:

const formData = reactive({ name: 'Alice', email: 'alice@example.com' })

async function submit() {
  // Pass the raw object to the API, not the Proxy
  await fetch('/api/submit', { body: JSON.stringify(toRaw(formData)) })
}

toRaw does not deep-clone. If the raw object is mutated, the changes are visible through the Proxy, but the Proxy’s reactivity does not track those mutations. If a copy is needed, the object should be cloned after toRaw .


c. The Relationship Between the Two

markRaw and toRaw address the same problem from opposite directions. markRaw is preventive: it stops the Proxy from being created. toRaw is remedial: it retrieves the original from a Proxy that has already been created. The choice depends on when the developer has control over the object.

If the object is created or assigned by the developer, markRaw is the correct tool. It prevents the Proxy from ever existing, which avoids the overhead and the identity issues entirely. If the object has already been made reactive by Vue—for example, because it was placed in reactive() before the developer realized it needed to be raw—toRaw is the recovery mechanism. It retrieves the original for the specific operation that needs it, without changing the object’s reactive status.

The Vue documentation and community guidance consistently recommend markRaw over toRaw for objects that should never be reactive. markRaw is a declaration of intent; toRaw is a workaround for a situation that was not declared .


Complete Example Session

// ============================================
// PART 1: markRaw PREVENTS CONVERSION
// ============================================
import { markRaw, reactive, isReactive } from 'vue'

const raw = markRaw({ count: 0 })
const state = reactive({ data: raw })

console.log(isReactive(state.data)) // false


// ============================================
// PART 2: LIBRARY INSTANCE WITH markRaw
// ============================================
import { markRaw, reactive } from 'vue'
import mapboxgl from 'mapbox-gl'

const state = reactive({
  map: markRaw(new mapboxgl.Map({ container: 'map' })),
})
// The map instance is not proxied; its internals remain intact


// ============================================
// PART 3: toRaw RETRIEVES THE ORIGINAL
// ============================================
import { reactive, toRaw } from 'vue'

const form = reactive({ name: 'Alice' })
const plain = toRaw(form)

plain.name = 'Bob'
console.log(form.name) // 'Alice' (Proxy unchanged)
console.log(plain.name) // 'Bob'


// ============================================
// PART 4: toRaw FOR API SUBMISSION
// ============================================
async function submit(formData) {
  // The API expects a plain object, not a Proxy
  const payload = toRaw(formData)
  await fetch('/api/submit', { body: JSON.stringify(payload) })
}


// ============================================
// PART 5: COMPONENT REFERENCE WITH markRaw
// ============================================
import { markRaw, reactive, type Component } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

const tabs = reactive([
  { name: 'A', component: markRaw(ComponentA) },
  { name: 'B', component: markRaw(ComponentB) },
])
// No "Component made reactive" warning


// ============================================
// PART 6: markRaw LIMITATION
// ============================================
const rawObj = markRaw({ nested: { value: 1 } })
const container = reactive({ data: rawObj })
// container.data is not reactive
// container.data.nested may still be proxied in some cases
// Safer: use shallowRef for the container


// ============================================
// PART 7: SHALLOWREF + MARKRAW PATTERN
// ============================================
import { shallowRef, markRaw, onMounted } from 'vue'

const map = shallowRef(null)
onMounted(() => {
  map.value = markRaw(new MapboxMap({ container: 'map' }))
})
// .value replacement is tracked; instance is never proxied

The seven parts trace the preventive and remedial approaches, the two common integration patterns, and the limitation of markRaw at the root level.


Quick Reference

markRaw vs toRaw

AspectmarkRawtoRaw
DirectionPrevents Proxy creationRetrieves original from Proxy
When to useAt creation/assignmentAt consumption
EffectObject never proxiedObject unwrapped for one operation
PersistentPermanent for object lifetimeTemporary; Proxy remains
Common useLibrary instances, componentsAPI payloads, external libraries

When to Use Each

ScenarioUse
Third-party library instancemarkRaw
Vue component in reactive statemarkRaw or shallowRef
DOM node in reactive statemarkRaw
Passing reactive data to APItoRaw
Class with private fieldsmarkRaw
Large static datamarkRaw or shallowRef

Related Utilities

UtilityPurpose
shallowRefOnly .value reactive; inner object not proxied
shallowReactiveOnly root properties reactive
isReactiveCheck if a Proxy
triggerRefForce update on a shallow ref

Best Practices

✅ Do This:

// Mark library instances as raw
const map = markRaw(new MapboxMap())                                  // ✅
// Use toRaw for API payloads
await fetch('/api', { body: JSON.stringify(toRaw(state)) })            // ✅
// Use markRaw for components in reactive arrays
const tabs = reactive([{ component: markRaw(ComponentA) }])            // ✅
// Combine shallowRef and markRaw for external instances
const map = shallowRef(null)
map.value = markRaw(new MapboxMap())                                   // ✅

❌ Don’t Do This:

// Store a library instance in reactive without markRaw
const state = reactive({ map: new MapboxMap() })  // breaks internals   // ❌
// Hold a persistent reference to a toRaw object
const raw = toRaw(state)  // diverges from the Proxy over time         // ❌
// Use ref for dynamic component references
const comp = ref(ComponentA)  // warning + overhead                    // ❌
// Assume markRaw affects nested objects
markRaw({ nested: {} })  // nested may still be proxied                // ❌

Common Pitfalls

PitfallWhy It HappensFix
Library breaks after Proxy wrapIdentity check failsmarkRaw the instance
Component warning in consoleComponent stored in ref/reactivemarkRaw or shallowRef
toRaw object is staleProxy and raw divergeClone after toRaw if needed
Nested object still proxiedmarkRaw only root levelUse shallowRef as container
API receives ProxyJSON.stringify on reactive objecttoRaw before serialization

Real-World Examples

1. Mapbox Instance

const map = markRaw(new mapboxgl.Map({ container: 'map' }))

2. CodeMirror Instance

const editor = markRaw(EditorView({ state }))

3. Component in Reactive Array

const tabs = reactive([{ component: markRaw(ComponentA) }])

4. toRaw for API

await fetch('/api', { body: JSON.stringify(toRaw(form)) })

5. DOM Node

const elements = reactive({ container: markRaw(document.getElementById('app')) })

6. WebSocket Manager

const ws = markRaw(new WebSocketManager(url))

7. Large Static Data

const geo = markRaw(await fetch('/huge.json').then(r => r.json()))

8. ShallowRef + markRaw

const map = shallowRef(null)
map.value = markRaw(new MapboxMap())

9. Check Reactivity

isReactive(state.map) // false

10. Clone After toRaw

const plain = JSON.parse(JSON.stringify(toRaw(state)))

Visual

markRaw vs toRaw

┌──────────────────────────────────────────────────────────────┐
│  markRaw                        toRaw                        │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ raw object      │            │ Proxy           │         │
│  │    │            │            │    │            │         │
│  │    ▼            │            │    ▼            │         │
│  │ markRaw()       │            │ toRaw()         │         │
│  │    │            │            │    │            │         │
│  │    ▼            │            │    ▼            │         │
│  │ Never proxied   │            │ Original object │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  Preventive                     Remedial                      │
│  At creation                    At consumption                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Identity Problem

┌──────────────────────────────────────────────────────────────┐
│  WITHOUT markRaw                                             │
│                                                              │
│  Library creates instance → Vue wraps in Proxy               │
│  Library checks: instance === stored?                        │
│  Proxy !== raw target → check fails → bug                   │
│                                                              │
│  WITH markRaw                                                │
│                                                              │
│  Library creates instance → markRaw → Vue skips              │
│  Library checks: instance === stored?                        │
│  raw === raw → check passes → works                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

markRaw Root-Level Limitation

┌──────────────────────────────────────────────────────────────┐
│  const raw = markRaw({ nested: { value: 1 } })               │
│  const state = reactive({ data: raw })                       │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ state.data      │  Not proxied (marked raw)              │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ state.data      │  May still be proxied                  │
│  │ .nested         │  (accessed through reactive parent)    │
│  └─────────────────┘                                        │
│                                                              │
│  SAFER: shallowRef(raw)                                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Component Storage

┌──────────────────────────────────────────────────────────────┐
│  ❌ ref(ComponentA)                                          │
│  → warning: "Component made reactive"                        │
│  → deep proxy of component internals                         │
│                                                              │
│  ✅ shallowRef(ComponentA)                                   │
│  → no warning                                                │
│  → .value replacement tracked                                │
│  → component internals not proxied                           │
│                                                              │
│  ✅ markRaw(ComponentA) in reactive array                    │
│  → component not proxied                                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
markRawPrevents Proxy conversion permanently
toRawRetrieves original from an existing Proxy
markRaw directionPreventive (at creation)
toRaw directionRemedial (at consumption)
Library instancesmarkRaw
API payloadstoRaw
Component referencesmarkRaw or shallowRef
markRaw limitationRoot level only
toRaw warningDo not hold persistent reference

Key takeaways:

  • markRaw prevents the Proxy from being created. It marks an object so that Vue’s reactivity system skips it, even when nested inside a reactive structure. The object’s identity is preserved, its this binding remains correct, and its internal state is not tracked .
  • toRaw retrieves the original from an existing Proxy. It is the escape hatch for passing reactive data to an API, a non-Vue library, or any code that cannot handle a Proxy. The returned object is not reactive, and mutations to it do not trigger updates .
  • The choice depends on timing. If the developer controls the object at creation, markRaw is the correct declaration of intent. If the object has already been made reactive, toRaw is the recovery mechanism for the specific operation that needs the raw object. The Vue community consistently recommends markRaw over toRaw for objects that should never be reactive .
  • markRaw only affects the root level. A nested object inside a markRaw object may still be proxied if accessed through a reactive parent. The safer container for objects that should never be reactive is shallowRef combined with markRaw .
  • External library instances and component definitions are the primary use cases. Mapbox maps, CodeMirror editors, WebSocket managers, and Vue component references all break or warn when wrapped in a Proxy. markRaw and shallowRef are the correct storage mechanisms for these objects .

Remember: markRaw and toRaw are not reactivity features; they are reactivity boundaries. They define where Vue’s tracking stops. The Proxy is the default, and these functions are the exceptions that let objects pass through the reactive system without being transformed by it. The choice between them is a choice about control: markRaw is the declaration that an object is outside the reactive world, and toRaw is the temporary retrieval of the original for a specific operation that cannot handle the Proxy. When the boundary is drawn at the right place—around library instances, components, and external data—the reactivity system tracks what it should and leaves the rest alone.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!