| |

Vue.js 86 🟢 Low-Level Reactive Utilities (unref, toRef, toRefs, isRef, isReactive, isReadonly)

⏱️ Estimated read: 11 min | |

Vue’s reactivity system exposes a small set of utility functions for inspecting and normalizing reactive values. These functions are not the ones application code uses most often—ref, computed, and reactive are—but they are the ones composables and libraries use to handle arguments that might be a raw value, a ref, or a reactive object. The distinction between a ref and a reactive object is not cosmetic: a ref is a wrapper with a .value property, and a reactive object is a Proxy whose properties are accessed directly. The utilities in this chapter exist to bridge the two representations and to answer questions about what a value actually is.

Key point: unref unwraps a ref but leaves a reactive object untouched. toRef creates a ref that stays synchronized with a property on a reactive object, while toRefs does the same for every property at once. The is* functions are type guards: they narrow the TypeScript type of a value based on what it actually is at runtime.


Why these utilities exist

The composable argument problem. A composable that accepts a value often wants to support both a plain value and a ref. If the caller passes 1, the composable should use 1. If the caller passes ref(1), the composable should use the inner value and stay reactive to changes. unref is the one-line solution: it returns the inner value if the argument is a ref, and the argument itself otherwise . Without it, the composable would need an explicit isRef check and a .value access.

The destructuring problem. A reactive object loses its reactivity when it is destructured. const { name } = reactive({ name: 'Alice' }) extracts the current string value, not a live connection to the property. toRefs solves this by converting every property into a ref that stays synchronized with the source object . The destructured refs remain reactive because they are connected to the original Proxy.

The props problem. A component’s props object is reactive, but passing props.foo to a composable passes the current value, not a live reference. toRef(props, 'foo') creates a ref that stays synchronized with the prop, so the composable can read the current value through .value . The getter form, toRef(() => props.foo), is the recommended syntax in Vue 3.3 and later because it avoids the object-and-key signature and works with computed expressions .


a. Normalization: unref and isRef

isRef is a type guard that checks whether a value is a ref object. Its return type is a TypeScript type predicate, which means it narrows the type of the variable inside an if block .

import { isRef } from 'vue'

let value: unknown = ref(1)

if (isRef(value)) {
  // TypeScript knows value is Ref<number> here
  console.log(value.value) // 1
}

unref is the normalization function. It returns the inner value if the argument is a ref, and the argument itself otherwise. The Vue documentation describes it as the sugar function for val = isRef(val) ? val.value : val .

import { unref, ref } from 'vue'

const count = ref(42)
const plain = 42

console.log(unref(count)) // 42
console.log(unref(plain)) // 42

The distinction that matters is between a ref and a reactive object. unref(reactive({ a: 1 })) returns the reactive object itself, not a plain object. The function only unwraps refs. A reactive object is already “unwrapped” in the sense that its properties are accessed directly.


b. Synchronization: toRef and toRefs

toRef has two signatures. The object property signature takes a reactive object and a key, and returns a ref that is synchronized with that property . The normalization signature, introduced in Vue 3.3, takes a value, a ref, or a getter and returns a ref .

import { reactive, toRef } from 'vue'

const state = reactive({ foo: 1, bar: 2 })

// Two-way ref synced with state.foo
const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

The synchronization is the difference between toRef(state, 'foo') and ref(state.foo). The ref() call receives a plain number, so the resulting ref is disconnected from state.foo. The toRef() call creates a ref whose .value getter reads from the source and whose .value setter writes back to it .

toRefs applies toRef to every enumerable property of a reactive object and returns a plain object whose properties are all refs . This is the function that makes destructuring safe.

import { reactive, toRefs } from 'vue'

const state = reactive({ name: 'Alice', age: 30 })
const { name, age } = toRefs(state)

// Both are refs, both are synced
name.value = 'Bob'
console.log(state.name) // 'Bob'

A limitation of toRefs is that it only includes properties that exist at the time it is called. Optional properties that are not yet present are not converted. The toRef object signature does not have this limitation: it returns a usable ref even if the source property does not currently exist, which makes it the correct choice for optional props .


c. Inspection: isReactive and isReadonly

isReactive checks whether a value is a reactive proxy created by reactive or shallowReactive. isReadonly checks whether a value is a readonly proxy created by readonly or shallowReadonly .

import { reactive, readonly, isReactive, isReadonly } from 'vue'

const state = reactive({ a: 1 })
const readonlyState = readonly({ a: 1 })

console.log(isReactive(state)) // true
console.log(isReactive(readonlyState)) // false
console.log(isReadonly(readonlyState)) // true

The interaction between the two functions is worth noting. A reactive object wrapped in readonly is both reactive and readonly: isReactive(readonly(reactive({}))) returns true, and isReadonly(readonly(reactive({}))) also returns true . The readonly proxy preserves the underlying reactive proxy, and both checks report their respective truth. A readonly proxy created from a plain object is readonly but not reactive.

The readonly proxy is deep by default. Accessing a nested object through a readonly proxy returns a readonly proxy for the nested object as well. The shallowReadonly variant makes only the first level readonly and leaves nested objects mutable and non-reactive .


Complete Example Session

// ============================================
// PART 1: isRef AS A TYPE GUARD
// ============================================
import { ref, isRef } from 'vue'

let value: unknown = ref('hello')

if (isRef(value)) {
  // TypeScript narrows the type
  console.log(value.value) // 'hello'
}


// ============================================
// PART 2: unref NORMALIZATION
// ============================================
import { unref, ref } from 'vue'

function useFeature(input: number | Ref<number>) {
  // Works for both plain values and refs
  const value = unref(input)
  return value * 2
}

useFeature(21)        // 42
useFeature(ref(21))   // 42


// ============================================
// PART 3: toRef SYNCHRONIZATION
// ============================================
import { reactive, toRef } from 'vue'

const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')

countRef.value = 5
console.log(state.count) // 5

state.count = 10
console.log(countRef.value) // 10


// ============================================
// PART 4: toRef GETTER FORM (3.3+)
// ============================================
import { toRef } from 'vue'

const props = defineProps<{ title: string }>()

// Read-only ref that tracks the prop
const titleRef = toRef(() => props.title)


// ============================================
// PART 5: toRefs FOR DESTRUCTURING
// ============================================
import { reactive, toRefs } from 'vue'

const user = reactive({ name: 'Alice', age: 30 })
const { name, age } = toRefs(user)

name.value = 'Bob'
console.log(user.name) // 'Bob'


// ============================================
// PART 6: toRef FOR OPTIONAL PROPERTIES
// ============================================
// toRef works even if the key does not exist yet
const optionalRef = toRef(state, 'optionalKey')
console.log(optionalRef.value) // undefined
optionalRef.value = 'now set'
console.log(state.optionalKey) // 'now set'


// ============================================
// PART 7: isReactive AND isReadonly
// ============================================
import { reactive, readonly, isReactive, isReadonly } from 'vue'

const normal = reactive({ a: 1 })
const ro = readonly({ a: 1 })
const roOfReactive = readonly(normal)

console.log(isReactive(normal))       // true
console.log(isReactive(ro))           // false
console.log(isReadonly(ro))           // true
console.log(isReactive(roOfReactive)) // true
console.log(isReadonly(roOfReactive)) // true


// ============================================
// PART 8: READONLY IS DEEP
// ============================================
const deep = readonly({ nested: { value: 1 } })
// deep.nested.value = 2 // Warning: Set operation on key failed
console.log(isReadonly(deep.nested)) // true


// ============================================
// PART 9: COMPOSABLE WITH unref
// ============================================
import { unref, isRef } from 'vue'

function useCounter(initial: number | Ref<number>) {
  // Normalize once
  const start = unref(initial)
  const count = ref(start)

  return { count }
}


// ============================================
// PART 10: COMPOSABLE WITH toRef FOR PROPS
// ============================================
import { toRef } from 'vue'

function useTitle(props: { title: string }) {
  // Stays synced with props.title
  const title = toRef(() => props.title)
  return { title }
}

The ten parts trace the normalization, synchronization, and inspection functions. Parts 2 and 3 show the difference between unref (unwraps a ref) and toRef (creates a synchronized ref). Part 7 shows the interaction between isReactive and isReadonly.


Quick Reference

The Utility Functions

FunctionSignaturePurpose
isRef(r) => r is Ref<T>Type guard for refs
unref(v) => TUnwrap ref, return value otherwise
toRef(obj, key) => RefSynced ref for one property
toRef(getter) => RefRead-only ref from getter (3.3+)
toRefs(obj) => { [K]: Ref }Synced refs for all properties
isReactive(v) => booleanCheck for reactive proxy
isReadonly(v) => booleanCheck for readonly proxy

Ref vs. Reactive Object

AspectRefReactive Object
Access.valueDirect properties
Created byref()reactive()
isReftruefalse
unrefReturns inner valueReturns object unchanged
Auto-unwrapped in templateYesN/A

toRef vs. ref

AspecttoRef(state, 'foo')ref(state.foo)
Synced with sourceYesNo
Two-way bindingYesNo
Works for missing keysYesN/A
Use casePass property to composableSnapshot value

Best Practices

✅ Do This:

// Use unref for composable arguments that may be refs
function useFeature(v: number | Ref<number>) {
  return unref(v) * 2
}                                                                       // ✅
// Use toRefs to destructure without losing reactivity
const { name, age } = toRefs(reactiveUser)                             // ✅
// Use toRef with a getter for props in composables (3.3+)
useSomeFeature(toRef(() => props.foo))                                 // ✅
// Use isReadonly to guard against mutation
if (!isReadonly(state)) {
  state.value++
}                                                                       // ✅

❌ Don’t Do This:

// Destructure a reactive object directly
const { name } = reactiveUser  // name is not reactive                    // ❌
// Use ref() when you need a synced property ref
const fooRef = ref(state.foo)  // disconnected from state.foo            // ❌
// Assume toRefs includes optional keys
const refs = toRefs(state)  // only includes existing keys              // ❌

Common Pitfalls

PitfallWhy It HappensFix
Destructured value is staleconst { x } = reactive() loses reactivitytoRefs()
Ref is not synced with sourceref(state.foo) copies the valuetoRef(state, 'foo')
Optional prop not convertedtoRefs only includes existing keystoRef(state, 'optionalKey')
unref does not unwrap reactiveunref only handles refsAccess properties directly
Readonly check fails on nestedisReadonly checks the proxyNested objects are also readonly

Real-World Examples

1. isRef Type Guard

if (isRef(value)) { value.value }

2. unref Plain Value

unref(42) // 42

3. unref Ref

unref(ref(42)) // 42

4. toRef Synced Property

const fooRef = toRef(state, 'foo')

5. toRef Getter

const title = toRef(() => props.title)

6. toRefs Destructure

const { x, y } = toRefs(state)

7. isReactive

isReactive(reactive({})) // true

8. isReadonly

isReadonly(readonly({})) // true

9. isReactive on Readonly of Reactive

isReactive(readonly(reactive({}))) // true

10. Composable with unref

function useInput(v: string | Ref<string>) { return unref(v) }

Visual

Ref vs. Reactive

┌──────────────────────────────────────────────────────────────┐
│  REF                          REACTIVE OBJECT                │
│                                                              │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │ { value: 42 }   │          │ Proxy { a: 1 }  │           │
│  └────────┬────────┘          └────────┬────────┘           │
│           │                            │                     │
│           ▼                            ▼                     │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │ count.value     │          │ state.a         │           │
│  └─────────────────┘          └─────────────────┘           │
│                                                              │
│  unref(count) → 42            unref(state) → state           │
│  isRef(count) → true          isRef(state) → false           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

toRef Synchronization

┌──────────────────────────────────────────────────────────────┐
│  state.foo = 1                                               │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────┐                                        │
│  │ toRef(state,    │                                        │
│  │ 'foo')          │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ fooRef.value    │  Two-way binding                       │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│  fooRef.value++ → state.foo++                               │
│  state.foo++    → fooRef.value++                            │
│                                                              │
│  ref(state.foo) copies the value. No sync.                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

toRefs Destructuring

┌──────────────────────────────────────────────────────────────┐
│  reactive({ name: 'Alice', age: 30 })                        │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────┐                                        │
│  │ toRefs()        │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ { name: Ref,    │                                        │
│  │   age: Ref }    │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  const { name, age } = toRefs(state)                        │
│                                                              │
│  Destructured refs stay connected to the source.             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

isReactive and isReadonly Interaction

┌──────────────────────────────────────────────────────────────┐
│  reactive({})                                                │
│  ├── isReactive: true                                       │
│  └── isReadonly: false                                      │
│                                                              │
│  readonly({})                                                │
│  ├── isReactive: false                                      │
│  └── isReadonly: true                                       │
│                                                              │
│  readonly(reactive({}))                                      │
│  ├── isReactive: true   ← underlying is reactive            │
│  └── isReadonly: true   ← outer proxy is readonly           │
│                                                              │
│  The readonly wrapper preserves the reactive base.           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
isRefType guard for ref objects
unrefUnwrap ref, return value otherwise
toRef(obj, key)Synced ref for one property
toRef(getter)Read-only ref from getter (3.3+)
toRefs(obj)Synced refs for all properties
isReactiveCheck for reactive proxy
isReadonlyCheck for readonly proxy
toRef vs refSynced vs. snapshot
toRefs limitationOnly existing keys
Readonly depthDeep by default

Key takeaways:

  • unref is the normalization function. It unwraps a ref and returns a plain value, and it returns non-ref values unchanged. It is the one-line solution for composables that accept either a value or a ref. It does not unwrap reactive objects; those are accessed directly .
  • toRef creates a synchronized ref, ref creates a snapshot. toRef(state, 'foo') stays connected to state.foo in both directions. ref(state.foo) copies the current value and loses the connection. The difference is invisible until the source changes .
  • toRefs makes destructuring safe. Destructuring a reactive object extracts plain values, which are not reactive. toRefs converts every property to a ref, and the destructured refs remain connected to the source. The limitation is that only existing keys are included .
  • isReactive and isReadonly are independent checks. A readonly proxy created from a reactive object is both reactive and readonly. The readonly wrapper does not remove the underlying reactive nature; it adds a read-only layer on top .
  • The readonly proxy is deep by default. Nested objects accessed through a readonly proxy are also readonly. The shallowReadonly variant makes only the first level readonly and leaves nested objects mutable .

Remember: The low-level reactivity utilities are the vocabulary that composables and libraries use to speak about values that might be refs or reactive objects. isRef asks what a value is. unref normalizes it. toRef and toRefs create synchronized references to properties that would otherwise lose their reactivity when extracted. isReactive and isReadonly inspect the proxy layer. None of these functions creates reactivity; they describe and bridge the reactivity that ref and reactive have already created. The distinction between a ref and a reactive object is the foundation, and the utilities are the tools that make the distinction manageable in code that receives values from callers it does not control.

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!