ref()

Use ref() method to create reactive objects that contain the values for the components state. When used inside script tags, the .value property must be accessed to read and mutate. When used in template you can use the reference directly (it is automatically unwrapped).

<script setup>
import { ref } from 'vue'

const count = ref(0)

const increment = () => {
  count.value++
}
</script>
<template>
  <button @click="count++">
    {{ count }}
  </button>
  <button @click="increment">
    {{ count }}
  </button>
</template>

reactive()

Use reactive() to create references to objects, arrays, and collections (like Map and Set). It is deeply reactive by nature, and it avoids the use of .value, because the object itself is made reactive. Also, with the use of toRefs() it is possible to destructure the object properties and still keeping the reactivity while modifying either the reactive object or the destructured refs.

<script setup>
import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0 })

// Destructuring while keeping reactivity
const state = reactive({ a: 1, b: 2 });
const { a, b } = toRefs(state);
</script>
<template>
  <button @click="state.count++">
    {{ state.count }}
  </button>
</template>

Reactivity Depth

Objects created with ref() and reactive() are deeply reactive by nature, this means, when some nested property is modified the change is detected reactively in all dependencies. If you want to avoid this behavior you can opt for using shallowRef() or shallowReactive(), which triggers changes only when the main .value is modified (ref) or the complete object is replaced (reactive).

import { ref } from 'vue'

const refObj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateRef() {
  // these will work as expected
  refObj.value.nested.count++
  refObj.value.arr.push('baz')
}

const shallowRefObj = shallowRef({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateShallowRef() {
  // this won't trigger change
  shallowRefObj.value.nested.count++
  // this will work as expected
  shallowRefObj.value = {
    nested: { count: 1 },
    arr: ['foo', 'bar', 'baz']
  }
}

Considerations

When an object is used to create a reactive object, both are not equal.

const raw = {}
const proxy = reactive(raw)

// Proxy is NOT equal to the original.
console.log(proxy === raw) // false

// Calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true

// Calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true

// This rule applies to nested objects as well
const proxy2 = reactive({})
const raw2 = {}
proxy2.nested = raw2
console.log(proxy2.nested === raw2) // false

When the reactive object value is entirely replaced, the reactivity is lost.

let state = reactive({ count: 0 })

// Reactivity lost!
state = reactive({ count: 1 })

When a ref is part of a collection, the values is accessed through .value.

const books = reactive([ref('Vue 3 Guide')])
// Need .value here
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// Need .value here
console.log(map.get('count').value)

When refs are used in template expressions, the unwrapping of a ref property is made with .value, and not accessing directly the property as in reactive() objects.

const count = ref(0)
const object = { id: ref(1) }

// Unwrapped correctly
{{ count + 1 }}

// Unwrapped correctly
{{ object.id.value + 1 }}

DOM Update Timing

DOM updates are performed asynchronously, and some times the access to a state that has not been updated can cause some errors. In order to wait all DOM updates, you can use nextTick().

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // Now the DOM is updated
}

References