ref

A ref holds one reactive value: .value in script, bare in the template, and Vue re-renders whatever read it.

Syntaxfunction ref<T>(value: T): Ref<UnwrapRef<T>>

A ref is a reactive container for a single value. You call ref(initial) and get back an object with one property, value. Read or write that property and Vue knows about it.

Vue needs the wrapper because plain JavaScript gives it no way to notice let count = 0; count++. A property access can be intercepted; a variable assignment cannot. So the value lives on an object and Vue watches the property.

Where .value is needed

In <script setup> you always write count.value. In the template you write {{ count }}: the compiler unwraps top-level refs for you, so {{ count.value }} there is wrong.

When an interpolation or a directive (an attribute starting with v- that Vue gives behaviour to) reads a ref during render, Vue records that this component depends on it. Writing a new value marks the component stale, and it re-renders.

What trips people up

Unwrapping is top-level only. const box = { height: ref(40) } leaves box.height as a ref object in an attribute binding, so there you need box.height.value.

A ref holding an object is deeply reactive: profile.value.tags.push('admin') is tracked. It can also be replaced outright with profile.value = { ... } — a ref survives reassignment, which is the main reason it is the default way to declare state.

Example

Example · vue
<!-- src/components/Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const step = ref(1)

function increment(): void {
  count.value += step.value
}

function reset(): void {
  count.value = 0
}
</script>

<template>
  <p>Counted {{ count }} so far</p>
  <!-- @click is shorthand for v-on:click; the two are the same thing -->
  <button type="button" @click="increment">add {{ step }}</button>
  <button type="button" v-on:click="reset">reset</button>
</template>

<style scoped>
button {
  margin-right: 0.5rem;
  font: inherit;
}
</style>

When to use it

  • Hold a counter, a loading flag or a selected id that the template renders.
  • Keep a draft record in one place so clearing the form is a single assignment.
  • Store a value an event handler writes and several parts of the template read.

More examples

A ref holding an object

A ref is not limited to primitives. The object inside it is made deeply reactive, so nested writes update the view, and the ref itself can be swapped for a fresh object in one assignment. Reach for this whenever a group of fields is cleared and refilled together.

Example · ts
// src/lib/profile.ts
import { ref } from 'vue'

const profile = ref({ name: 'Ada', tags: ['owner'] })

// Deep: a write to a nested array is tracked.
profile.value.tags.push('admin')
console.log(profile.value.tags.length)
// Output: 2

// A ref can be replaced wholesale and stays reactive.
profile.value = { name: 'Grace', tags: [] }
console.log(profile.value.name)
// Output: Grace

Unwrapping stops at the top level

Shows the exact boundary of template unwrapping: only refs exposed as top-level bindings are unwrapped everywhere; a ref reached through a plain object unwraps in text interpolation but not in a bound attribute. The fix is to expose the ref itself rather than burying it in an object.

Example · vue
<!-- src/components/BoxReadout.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const width = ref(120)
const box = { height: ref(40) }
</script>

<template>
  <!-- width is a top-level ref, so it unwraps -->
  <p>width: {{ width }}</p>
  <!-- box.height is nested: in an attribute binding you need .value -->
  <progress :value="box.height.value" max="100"></progress>
  <!-- inside {{ }} a nested ref does unwrap -->
  <p>height: {{ box.height }}</p>
</template>

Typing a ref that starts empty

When a ref starts as null or an empty array, inference gives you a type too narrow to assign to later. Pass the type argument, as in ref<Task | null>(null), or annotate the variable as Ref<Task[]>. Both forms produce the same ref; use whichever reads better in the file.

Example · ts
// src/lib/tasks.ts
import { ref } from 'vue'
import type { Ref } from 'vue'

interface Task {
  id: number
  title: string
  done: boolean
}

const selected = ref<Task | null>(null)
const tasks: Ref<Task[]> = ref([])

tasks.value.push({ id: 1, title: 'Write lesson', done: false })
selected.value = tasks.value[0] ?? null
console.log(selected.value?.title)
// Output: Write lesson

Discussion

  • Be the first to comment on this lesson.