Vue.js 89 🟢 Raw Object Escaping with markRaw() and toRaw()
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
| Aspect | markRaw | toRaw |
|---|---|---|
| Direction | Prevents Proxy creation | Retrieves original from Proxy |
| When to use | At creation/assignment | At consumption |
| Effect | Object never proxied | Object unwrapped for one operation |
| Persistent | Permanent for object lifetime | Temporary; Proxy remains |
| Common use | Library instances, components | API payloads, external libraries |
When to Use Each
| Scenario | Use |
|---|---|
| Third-party library instance | markRaw |
| Vue component in reactive state | markRaw or shallowRef |
| DOM node in reactive state | markRaw |
| Passing reactive data to API | toRaw |
| Class with private fields | markRaw |
| Large static data | markRaw or shallowRef |
Related Utilities
| Utility | Purpose |
|---|---|
shallowRef | Only .value reactive; inner object not proxied |
shallowReactive | Only root properties reactive |
isReactive | Check if a Proxy |
triggerRef | Force 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Library breaks after Proxy wrap | Identity check fails | markRaw the instance |
| Component warning in console | Component stored in ref/reactive | markRaw or shallowRef |
toRaw object is stale | Proxy and raw diverge | Clone after toRaw if needed |
| Nested object still proxied | markRaw only root level | Use shallowRef as container |
| API receives Proxy | JSON.stringify on reactive object | toRaw 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
| Item | Value |
|---|---|
markRaw | Prevents Proxy conversion permanently |
toRaw | Retrieves original from an existing Proxy |
markRaw direction | Preventive (at creation) |
toRaw direction | Remedial (at consumption) |
| Library instances | markRaw |
| API payloads | toRaw |
| Component references | markRaw or shallowRef |
markRaw limitation | Root level only |
toRaw warning | Do not hold persistent reference |
Key takeaways:
markRawprevents 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, itsthisbinding remains correct, and its internal state is not tracked .toRawretrieves 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,
markRawis the correct declaration of intent. If the object has already been made reactive,toRawis the recovery mechanism for the specific operation that needs the raw object. The Vue community consistently recommendsmarkRawovertoRawfor objects that should never be reactive . markRawonly affects the root level. A nested object inside amarkRawobject may still be proxied if accessed through a reactive parent. The safer container for objects that should never be reactive isshallowRefcombined withmarkRaw.- 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.
markRawandshallowRefare 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.
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!