A Tour of ref and reactive in Vue 3

One of the biggest questions when writing code with Vue 3 and the Composition API is what to use: ref or reactive?

The answer depends and the outcome could surprise you.


Below are two gists that serve as small tours of what ref and reactive do, how they do it, and how they interact with each other.

They are valid JavaScript and Vue 3 code with annotated explanations and console output to explain what’s happening at each point in the code.

You can run the gists on the Vue SFC Playground to experiment with them yourself in the browser. Links are available at the end of the article.

ref

// A Tour of ref()
import { ref, reactive } from 'vue'
/**
ref() - Type Definition:
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
**/
// ref() returns a Ref object with a .value property that contains the argument
let name = ref('Damian')
name.value === 'Damian' // true
// Primitives - number, bigint, boolean, string, null, undefined, symbol
ref(0)
ref(1234567890123456789012345678901234567890n)
ref(true)
ref('hello world')
ref(null)
ref(undefined)
ref(Symbol('id'))
/**
These all result in the same basic form of a Ref object with some internal flags and a value property:
RefImpl {
__v_isShallow: false,
dep: undefined,
__v_isRef: true,
_rawValue: 0,
_value: 0,
__proto__: { constructor: ƒ RefImpl(), value: 0 }
}
where value is the argument passed to the ref() function
**/
// Ref object instances are different even when their values are the same
ref(0) === ref(0) // false
// Objects - objects passed to ref() are first made reactive with reactive() and then wrapped in a Ref
let shape = {
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 },
}
let shapeRef = ref(shape)
/**
RefImpl {
__v_isShallow: false,
dep: undefined,
__v_isRef: true,
_rawValue: {
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
_value: Proxy [
{
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
],
__proto__: {
constructor: ƒ RefImpl(),
value: Proxy [
{
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
]
}
}
**/
// Note that the Ref's value is now a Proxy instance and not the original object argument
shapeRef.value === shape // false
shapeRef.value === reactive(shape) // true
// ref() with ref()
// Passing a Ref to ref() will result...
let newShapeRef = ref(shapeRef)
// ...in the same Ref being returned
newShapeRef === shapeRef // true
newShapeRef.value === shapeRef.value // true
// ref() with reactive()
// Passing a reactive object to ref() will result...
let reactiveShape = reactive(shape)
let reactiveShapeRef = ref(reactiveShape)
/**
RefImpl {
__v_isShallow: false,
dep: undefined,
__v_isRef: true,
_rawValue: {
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
_value: Proxy [
{
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
],
__proto__: {
constructor: ƒ RefImpl(),
value: Proxy [
{
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
]
}
}
**/
// ...in the reactive object being wrapped in a Ref
reactiveShape === reactiveShapeRef // false
reactiveShape === reactiveShapeRef.value // true
view raw tour_of_ref.js hosted with ❤ by GitHub

reactive

// A Tour of reactive()
import { reactive, ref, watch } from 'vue'
/**
reactive() - Type Definition:
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
**/
// reactive() returns a reactive object (an ES Proxy) that has the argument set as a target
const person = reactive({ name: 'Damian' })
person.name === 'Damian' // true
// Primitives - number, bigint, boolean, string, null, undefined, symbol
reactive(0)
reactive(1234567890123456789012345678901234567890n)
reactive(true)
reactive('hello world')
reactive(null)
reactive(undefined)
reactive(Symbol('id'))
/**
Primitives are not valid arguments for reactive(). It only accepts object types.
In development mode, passing a primitive will lead to a console warning and the original argument being returned.
Development mode output below:
'value cannot be made reactive: 0'
'value cannot be made reactive: 1234567890123456789012345678901234567890'
'value cannot be made reactive: true'
'value cannot be made reactive: hello world'
'value cannot be made reactive: null'
'value cannot be made reactive: undefined'
'value cannot be made reactive: Symbol(id)'
0
1234567890123456789012345678901234567890n
true
'hello world'
null
undefined
Symbol(id)
**/
// Objects - Supported object types: Object, Array, Set, Map, WeakMap, WeakSet
let shape = { type: 'circle', color: 'blue', coordinates: { x: 0, y: 0, z: 0 } }
let reactiveShape = reactive(shape)
/**
reactive() returns a (reactive) Proxy object. When dumping a Proxy into the console, it will include an array with the target and handler, respectively:
Proxy [
{
type: 'circle',
color: 'blue',
coordinates: { x: 0, y: 0, z: 0 }
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
]
**/
// The reactive Proxy is NOT the same target object
shape === reactiveShape // false
// Although it has access to values on the target object
shape.type === reactiveShape.type // true
shape.color === reactiveShape.color // true
// Any changes made to the target object will be reflected in the Proxy and vice versa
shape.type = 'triangle'
shape.type === reactiveShape.type // true
reactiveShape.color = 'red'
reactiveShape.color === shape.color // true
// Let's add a side effect to reactiveShape
// 'flush: sync' will run the effect immediately on every change
watch(reactiveShape, () => console.log('shape has changed'), { flush: 'sync' })
// You should always use the reactive Proxy to trigger effects on state changes
// otherwise any reactive effects will not work
reactiveShape.color = 'green' // 'shape has changed' logged to console
shape.color = 'purple'
// Property addition and deletion are tracked!
reactiveShape.x = 0 // 'shape has changed' logged to console
delete reactiveShape.x // 'shape has changed' logged to console
// Nested objects are also Proxies (it's *reactive* turtles all the way down!)
reactiveShape.coordinates
/**
Proxy [
{ x: 0, y: 0, z: 0 },
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
]
**/
shape.coordinates === reactiveShape.coordinates // false
shape.coordinates.x === reactiveShape.coordinates.x // true
shape.coordinates.y === reactiveShape.coordinates.y // true
shape.coordinates.z === reactiveShape.coordinates.z // true
// reactive() with reactive()
// reactive() returns the same Proxy for the same object argument
reactiveShape === reactive(shape) // true
// Calling reactive() with a reactive object will return the same reactive object
reactiveShape === reactive(reactiveShape) // true
// reactive() with ref()
// Passing a Ref to reactive() will result...
let countRef = ref(0)
let reactiveCount = reactive(countRef)
/**
Proxy [
RefImpl {
__v_isShallow: false,
dep: undefined,
__v_isRef: true,
_rawValue: 0,
_value: 0,
__proto__: { constructor: ƒ RefImpl(), value: 0 }
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
]
**/
// ...in a Proxy that targets our Ref object
reactiveCount === countRef // false
reactiveCount.value === countRef.value // true
// Objects may include nested Refs
let counter = { count: countRef, max: 10, min: 0 }
/**
{
count: RefImpl {
__v_isShallow: false,
dep: undefined,
__v_isRef: true,
_rawValue: 0,
_value: 0,
__proto__: { constructor: ƒ RefImpl(), value: 0 }
},
max: 10,
min: 0
}
**/
let reactiveCounter = reactive(counter)
/**
Proxy [
{
count: RefImpl {
__v_isShallow: false,
dep: undefined,
__v_isRef: true,
_rawValue: 0,
_value: 0,
__proto__: { constructor: ƒ RefImpl(), value: 0 }
},
max: 10,
min: 0
},
{
get: ƒ get(),
set: ƒ set(),
deleteProperty: ƒ deleteProperty(),
has: ƒ has(),
ownKeys: ƒ ownKeys()
}
]
Notice how count is still a Ref object
**/
// Refs within reactive objects are automatically unwrapped (no need to use .value) when accessed
reactiveCounter.count === 0 // true
reactiveCounter.count === countRef.value // true
// New Ref properties on the reactive object are unwrapped too!
reactiveCounter.interval = ref(1)
reactiveCounter.interval === 1 // true
// Ref unwrapping is done by the Proxy, the original counter object still needs to use .value
reactiveCounter.interval === counter.interval // false
reactiveCounter.interval === counter.interval.value // true

Playground

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s