In Vue 3, what is the difference between `markRaw()` and `toRaw()`, and when is each the right tool?
answer
- one prevents, one escapes
- a hidden skip flag
- the object behind the proxy
- temporary use only
- nested objects are not marked
basics
~20 smarkRaw(obj) flags an object so Vue never wraps it in a proxy, even inside reactive state. toRaw(proxy) returns the original object behind an existing proxy, for short untracked reads or writes. markRaw prevents proxying up front; toRaw steps around a proxy after the fact.
solid answer
~40 s`markRaw(obj)` sets a hidden skip flag on the object and returns it; `reactive()`, `ref()` and nested access through reactive state then hand back the object itself, never a proxy. Use it for values that should never be reactive: third-party class instances, component definitions, large static data. The flag covers only that object, not its nested objects. `toRaw(proxy)` returns the original object behind a proxy made by `reactive()`, `readonly()`, `shallowReactive()` or `shallowReadonly()`. It is an escape hatch: read without tracking overhead, write without triggering, or pass data to an API that cannot handle proxies. Do not keep the raw reference around - writes through it never update the UI.
code
ts · 12 linesimport { reactive, toRaw, markRaw, isReactive } from 'vue'
const draft = reactive({ title: 'Report', tags: ['q3'] })
// structuredClone throws on a proxy; clone the raw object instead
const snapshot = structuredClone(toRaw(draft))
// a static lookup table that should never be proxied
const countries = markRaw(new Map([['fr', 'France'], ['de', 'Germany']]))
const form = reactive({ countries, selected: 'fr' })
isReactive(form.countries) // false: the marked Map is returned as-isgo deeper
Recall that markRaw stops Vue from ever proxying an object, while toRaw gives you the original object behind a proxy.
Explain the skip flag, the root-only scope that causes identity hazards, and why writes through a raw reference do not trigger.
Use toRaw only at call sites for proxy-hostile APIs or hot loops, and review for stored raw references that leave the UI stale.
Define where raw objects are allowed in the state graph so opt-outs stay rare, documented and free of identity hazards.
## Two opposite directions Both functions deal with the boundary between an object and Vue's proxy for it, but they work in opposite directions: | | `markRaw(obj)` | `toRaw(proxy)` | |---|---|---| | When you call it | before the object enters reactive state | after you already hold a proxy | | What it returns | the same object, now flagged | the original object behind the proxy | | Lasting effect | yes: the object is never proxied again | none: the proxy is unchanged | | Typical use | library instances, component objects, static data | untracked reads, silent writes, handing data to non-proxy-safe code | ## `markRaw()`: never make this reactive `markRaw()` defines a non-enumerable skip flag on the object. When Vue is about to create a reactive or readonly proxy, it checks that flag and returns the object untouched. Because deep reactivity wraps nested objects lazily on access, the flag also works when the marked object sits inside reactive state: ```ts const foo = markRaw({}) const bar = reactive({ foo }) isReactive(bar.foo) // false ``` Good candidates, per the docs: complex third-party class instances and Vue component objects. Non-extensible objects (for example after `Object.freeze()`) are skipped the same way without any marking. **The identity hazard.** The opt-out applies only to the marked object itself. Its nested objects are ordinary, so if you place `foo.nested` into reactive state and read it back, you get a proxy: ```ts const foo = markRaw({ nested: {} }) const bar = reactive({ nested: foo.nested }) foo.nested === bar.nested // false ``` Code that compares the two, or uses one as a map key and looks it up with the other, breaks. ## `toRaw()`: step around the proxy `toRaw()` follows the proxy's internal pointer to the original object, recursively when proxies are stacked (a readonly proxy over a reactive one). The docs call it an escape hatch with three legitimate uses: - **Reading without overhead.** A tight loop over a large structure can read the raw object and skip a trap call and a `track()` per property. - **Writing without triggering.** A write to the raw object changes the data but notifies nobody - occasionally wanted for bookkeeping fields no view depends on. - **Handing data to code that cannot take proxies.** `structuredClone()` refuses a proxy with a `DataCloneError`, and some libraries compare identities or keep internal references; pass them the raw object. ## The rule: do not keep raw references The danger of `toRaw()` is keeping the result. A component that stores `const raw = toRaw(state)` and later writes `raw.count++` changes the data, but no `trigger()` runs, so every view reading `state.count` stays stale until something else re-renders it. Reads through `raw` inside a `computed` are not tracked either, so the computed never invalidates. Use `toRaw()` at the call site, for the duration of one operation. ## Choosing between them 1. Should this object **ever** be reactive? If not, `markRaw()` it where it is created. 2. Is it normally reactive, but **this one operation** must avoid the proxy? Use `toRaw()` inline. 3. Should only **replacement** of the object be reactive? That is a `shallowRef()`, not either of these. Comparing a proxy with its original for identity is a separate topic - the proxy-identity rules - and not the main reason to reach for `toRaw()` here. ## Common mistakes - Calling `toRaw()` on a plain object to "make it raw" before storing it in reactive state: it returns the same object, which Vue then proxies on access anyway. That job needs `markRaw()`. - Believing `markRaw()` protects everything reachable from the object. - Writing through a stored raw reference and wondering why the screen does not change. - Reaching for `markRaw()` to speed up a component when the real problem is how much data it renders; opt-outs change update behaviour, so measure first. ## A worked comparison Suppose a form keeps a `reactive()` draft and also needs a static list of 5,000 postal codes. The postal codes never change, so `markRaw()` them where they are loaded: Vue will never spend a proxy or a dependency record on them, and reading `form.codes` returns the real array. The draft is edited constantly and must stay reactive; when saving, you call `structuredClone(toRaw(draft))` once, at the call site, to take a snapshot the storage API can accept. One object was opted out for its whole life, the other only for a single operation - which is the whole difference between the two functions.
- What is the identity hazard the Vue docs warn about with `markRaw()`?markRaw only flags the object itself. Its nested objects are ordinary, so putting `foo.nested` into reactive state and reading it back gives a proxy, and `foo.nested === bar.nested` is false. Code that compares, or keys maps by, the raw and proxied versions then misbehaves.
- Why shouldn't a Vue component keep a long-lived reference returned by `toRaw()`?Writes through it never call `trigger()`, so views reading the reactive version stay stale, and reads through it are not tracked, so computeds and watchers built on them never update. The docs describe `toRaw()` as a temporary escape hatch, not a second handle on the state.
- Does `toRaw()` on a readonly proxy over reactive state return the reactive proxy or the plain object?The plain object. `toRaw()` follows the raw pointer recursively, so it unwraps the readonly proxy and then the reactive proxy beneath it, returning the original object.
saying these in an interview costs you the question
- markRaw() and toRaw() both return a non-reactive copy of the object.
- Calling toRaw() on an object before storing it keeps it unproxied later.
- markRaw() also protects every nested object inside the marked one.
- Writing through a toRaw() reference updates the view like any reactive write.
- Keeping a toRaw() reference for later use is a safe performance optimisation.