markRaw tells Vue to never wrap an object in a reactive Proxy. toRaw returns the original object behind an existing Proxy. Both exist because not everything belongs inside the reactivity system.
markRaw: prevent an object from becoming reactive
When you store a third-party library instance, a DOM element, or a large static dataset inside reactive state, Vue wraps it in a Proxy. This causes problems: library internals can break, identity checks fail, and you pay tracking overhead for data that will never trigger a re-render.
import { reactive, markRaw } from 'vue'
import mapboxgl from 'mapbox-gl'
// Wrong: Mapbox instance gets proxied, internal methods may break
const state = reactive({
map: new mapboxgl.Map({ container: 'map' })
})
// Right: markRaw prevents proxy wrapping
const state = reactive({
map: markRaw(new mapboxgl.Map({ container: 'map' }))
})Common candidates for markRaw
// Library instances (charts, editors, maps)
const editor = markRaw(monaco.editor.create(element, {}))
// Class instances with internal state
const ws = markRaw(new WebSocketManager('ws://example.com'))
// Large static datasets that never change
const geoData = markRaw(await fetch('/huge.json').then(r => r.json()))
// DOM elements stored in reactive state
const el = markRaw(document.getElementById('canvas')!)Best pattern: shallowRef + markRaw
shallowRef only tracks .value reassignment (not deep properties), and markRaw prevents the assigned object from being proxied:
import { shallowRef, markRaw, onMounted, onUnmounted } from 'vue'
function useChart(containerId: string) {
const chart = shallowRef<Chart | null>(null)
onMounted(() => {
chart.value = markRaw(new Chart(containerId, { /* config */ }))
})
onUnmounted(() => {
chart.value?.destroy()
})
return { chart }
}toRaw: access the original object behind a Proxy
toRaw strips the reactive Proxy and returns the underlying plain object. Use it when you need to pass data to something that shouldn't receive a Proxy (APIs, libraries, structured clone, comparison).
import { reactive, toRaw } from 'vue'
const state = reactive({ name: 'Ana', age: 30 })
// Send plain data to an API (no Proxy in the payload)
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(toRaw(state))
})
// structuredClone needs plain objects
const snapshot = structuredClone(toRaw(state))
// Identity comparison
const raw = toRaw(state)
console.log(raw === state) // false (state is a Proxy)markRaw vs toRaw
markRaw | toRaw | |
|---|---|---|
| When to use | Before storing in reactive state | After something is already reactive |
| What it does | Marks object so it's never proxied | Returns the plain object behind a Proxy |
| Permanent? | Yes, the mark stays on the object | No, it just unwraps once |
| Mutates the object? | Adds a __v_skip flag | No |
Gotcha: markRaw is shallow
markRaw only prevents the root object from being proxied. Nested objects can still be wrapped if accessed through a reactive parent:
const data = markRaw({ nested: { value: 1 } })
const state = reactive({ data })
// state.data won't be proxied
// but state.data.nested might be in some edge cases
// Safer: combine with shallowRef
const safeData = shallowRef(markRaw(data))See also: What happens when you use Object.freeze() on reactive data? · What is the reactivity proxy identity hazard?