| |

Vue.js 18 🟢 Reactive State Creation with ref()

Reactivity is the mechanism that connects a component’s state to its rendered output. When the state changes, the DOM updates. Vue’s reactivity system is built on proxies, and ref() is the function that creates a reactive reference to a value. It is the most common way to declare state in a <script setup> component, and understanding what it returns, how it is unwrapped, and how it interacts with the template is the foundation for everything else in the Composition API.

Key point: ref(value) returns a reactive object with a single property: .value. In the script, the value is read and written through .value. In the template, the ref is auto-unwrapped, so count in the template is the same as count.value in the script. A ref can hold any value type: primitives, objects, arrays, and even other refs. When the value inside a ref changes, Vue triggers a re-render of the components that depend on it.


Why ref() exists

The reactivity problem. A plain JavaScript variable is not reactive. Assigning to it does not notify anyone. A render function that reads the variable does not know when it changes. Vue’s reactivity system wraps the variable in a proxy that intercepts reads and writes, tracks which components depend on it, and triggers re-renders when it changes. ref() is the function that creates this wrapper for a single value.

The primitive problem. JavaScript primitives—numbers, strings, booleans—cannot be wrapped in a proxy. A proxy can only wrap an object. ref() solves this by boxing the primitive in an object with a .value property. The proxy is on the object; the .value property holds the primitive. Reading and writing .value goes through the proxy, which is what makes the primitive reactive.

The unwrapping problem. Writing count.value everywhere is verbose. In the template, Vue auto-unwraps the ref, so count is enough. In the script, the .value is required. The distinction is deliberate: the template is compiled to a render function that accesses .value automatically, while the script is plain JavaScript that must be explicit.

The consistency problem. ref() works for any value type. A number, a string, an object, an array—all are wrapped in a ref in the same way. The .value access is uniform. The reactivity is uniform. This consistency is why ref() is the recommended way to declare state, even for objects that reactive() could also wrap.

The composition problem. A composable function that encapsulates logic returns refs. The component that uses the composable reads and writes the refs. The refs are the interface between the composable and the component. The reactivity travels through the ref, so the component re-renders when the composable’s state changes.


a. Creating and using refs

A ref is created with the ref() function, which is imported from Vue.

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

const count = ref(0)
const name = ref('Alice')
const items = ref([1, 2, 3])
const user = ref({ id: 1, name: 'Alice' })
</script>

The ref() function takes an initial value and returns a ref object. The ref’s .value property holds the value. The initial value can be any type.

In the script, the value is read and written through .value.

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

const count = ref(0)

function increment() {
  count.value++
}

console.log(count.value) // 0
increment()
console.log(count.value) // 1
</script>

The count.value++ reads the current value, increments it, and writes it back. The write triggers the reactivity system, which schedules a re-render of any component that depends on count.

For an object or array, the ref wraps the entire object. The .value property is the object, and the object’s properties are accessed normally.

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

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

user.value.name = 'Bob'      // reactive
user.value = { name: 'Carol' } // reactive
</script>

Replacing the entire object (user.value = ...) is reactive. Mutating a property (user.value.name = ...) is also reactive, because the object inside the ref is made reactive by the ref’s proxy.

In the template, the ref is auto-unwrapped. The .value is not needed.

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <p>{{ count }}</p>
  <button @click="count++">Increment</button>
</template>

The template’s {{ count }} is compiled to count.value. The @click="count++" is compiled to count.value++. The unwrapping is automatic, and the template reads and writes the ref as if it were a plain variable.

The auto-unwrapping applies only to top-level refs in the template. A ref nested inside an object is not unwrapped.

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

const state = {
  count: ref(0),
  name: 'Alice'
}
</script>

<template>
  <p>{{ state.count }}</p>       <!-- ref object, not the value -->
  <p>{{ state.count.value }}</p> <!-- the value -->
</template>

The state.count is a ref object. The template does not unwrap it because it is not a top-level ref. The .value must be used explicitly. This is why top-level refs are preferred over refs nested in plain objects.


b. Reactivity and the .value access

The reactivity of a ref is triggered by writes to .value. The write goes through the proxy, which notifies the dependency tracking system.

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

const count = ref(0)

setTimeout(() => {
  count.value = 10 // triggers re-render
}, 1000)
</script>

After one second, the count.value is set to 10. The components that read count in their render function are re-rendered. The DOM updates to show 10.

A write of the same value does not trigger a re-render. Vue compares the new value to the old value with Object.is.

<script setup>
const count = ref(0)
count.value = 0 // no re-render

The Object.is comparison means that NaN is equal to NaN, and +0 and -0 are different. This is the same comparison that React uses for state updates.

The .value property is not the only way to access the ref. The unref() function returns the value if the argument is a ref, or the argument itself if it is not.

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

const count = ref(0)
console.log(unref(count))  // 0
console.log(unref(42))     // 42
</script>

The unref function is useful in composables that accept either a ref or a plain value.

The isRef() function checks whether a value is a ref.

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

const count = ref(0)
console.log(isRef(count))  // true
console.log(isRef(42))     // false
</script>

The isRef function is used in composables that need to distinguish between a ref and a plain value.

The shallowRef() function creates a ref that is only reactive at the top level. Mutating a nested property does not trigger a re-render.

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

const user = shallowRef({ name: 'Alice' })

user.value.name = 'Bob'     // no re-render
user.value = { name: 'Bob' } // re-render
</script>

The shallowRef is used for performance when the nested data does not need to be reactive. It is also used for large data structures where the deep reactivity would be expensive.

The triggerRef() function manually triggers the re-render of a shallowRef.

<script setup>
import { shallowRef, triggerRef } from 'vue'

const user = shallowRef({ name: 'Alice' })
user.value.name = 'Bob'
triggerRef(user) // manually trigger the re-render
</script>

The triggerRef is used when the nested data is mutated and the re-render is needed.


c. Refs in composables and toRefs

A composable is a function that returns refs. The component that uses the composable reads and writes the refs, and the reactivity travels through them.

// composables/useCounter.js
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

The composable returns a count ref and two functions. The component uses the composable:

<script setup>
import { useCounter } from './composables/useCounter'
const { count, increment, decrement } = useCounter(10)
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</template>

The count is a ref. The template auto-unwraps it. The increment and decrement are functions that mutate the ref. The reactivity is intact because the ref is returned from the composable and used in the component.

The toRefs() function converts a reactive object into an object of refs. This is useful when a composable returns a reactive object and the component wants to destructure it without losing reactivity.

import { reactive, toRefs } from 'vue'

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

The component destructures the refs:

<script setup>
import { useUser } from './composables/useUser'
const { name, age } = useUser()
</script>

<template>
  <p>{{ name }} is {{ age }}</p>
</template>

The toRefs preserves the reactivity when the reactive object is destructured. Without it, the destructured properties would be plain values that do not update.

The toRef() function creates a ref that is linked to a property of a reactive object.

import { reactive, toRef } from 'vue'

const user = reactive({ name: 'Alice' })
const nameRef = toRef(user, 'name')

nameRef.value = 'Bob' // updates user.name

The toRef is used when a single property of a reactive object needs to be passed as a ref.

A ref can be used in a computed property. The computed reads the ref’s .value and returns a new value. The computed is itself a ref.

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

const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

<template>
  <p>{{ fullName }}</p>
</template>

The fullName is a read-only ref. It updates when firstName or lastName changes. The computed’s .value is accessed in the template through the auto-unwrap.


Complete Example Session

<!-- ============================================ -->
<!-- PART 1: BASIC REF -->
<!-- ============================================ -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <p>{{ count }}</p>
</template>
<!-- ============================================ -->
<!-- PART 2: WRITING TO A REF -->
<!-- ============================================ -->
<script setup>
import { ref } from 'vue'
const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>
<!-- ============================================ -->
<!-- PART 3: OBJECT REF -->
<!-- ============================================ -->
<script setup>
import { ref } from 'vue'
const user = ref({ name: 'Alice', age: 30 })

function birthday() {
  user.value.age++
}
</script>

<template>
  <p>{{ user.name }} is {{ user.age }}</p>
  <button @click="birthday">Birthday</button>
</template>
<!-- ============================================ -->
<!-- PART 4: ARRAY REF -->
<!-- ============================================ -->
<script setup>
import { ref } from 'vue'
const items = ref(['a', 'b', 'c'])

function addItem() {
  items.value.push('d')
}
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item">{{ item }}</li>
  </ul>
  <button @click="addItem">Add</button>
</template>
<!-- ============================================ -->
<!-- PART 5: NESTED REF NOT UNWRAPPED -->
<!-- ============================================ -->
<script setup>
import { ref } from 'vue'
const state = { count: ref(0) }
</script>

<template>
  <p>{{ state.count.value }}</p>
</template>
<!-- ============================================ -->
<!-- PART 6: UNREF -->
<!-- ============================================ -->
<script setup>
import { ref, unref } from 'vue'
const count = ref(0)
console.log(unref(count)) // 0
</script>
<!-- ============================================ -->
<!-- PART 7: SHALLOW REF -->
<!-- ============================================ -->
<script setup>
import { shallowRef } from 'vue'
const user = shallowRef({ name: 'Alice' })

function update() {
  user.value.name = 'Bob'  // no re-render
  user.value = { name: 'Bob' } // re-render
}
</script>
<!-- ============================================ -->
<!-- PART 8: COMPOSABLE WITH REF -->
<!-- ============================================ -->
<script setup>
import { useCounter } from './composables/useCounter'
const { count, increment } = useCounter(10)
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
</template>
<!-- ============================================ -->
<!-- PART 9: TOREFS -->
<!-- ============================================ -->
<script setup>
import { reactive, toRefs } from 'vue'
const user = reactive({ name: 'Alice', age: 30 })
const { name, age } = toRefs(user)
</script>

<template>
  <p>{{ name }} is {{ age }}</p>
</template>
<!-- ============================================ -->
<!-- PART 10: COMPUTED FROM REFS -->
<!-- ============================================ -->
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

<template>
  <p>{{ fullName }}</p>
</template>

The ten parts covered a basic ref, writing to a ref, an object ref, an array ref, a nested ref, unref, shallowRef, a composable with a ref, toRefs, and computed from refs.


Quick Reference

Ref Functions

FunctionPurpose
ref(value)Create a reactive ref
shallowRef(value)Create a top-level-only reactive ref
unref(ref)Get the value
isRef(value)Check if a ref
toRef(obj, key)Ref linked to an object property
toRefs(obj)Object of refs from a reactive object
triggerRef(ref)Manually trigger a re-render

Access Patterns

ContextAccess
Scriptcount.value
Templatecount (auto-unwrapped)
Nested in objectstate.count.value
In composablecount.value

Ref Value Types

TypeExample
Numberref(0)
Stringref('Alice')
Booleanref(true)
Arrayref([1, 2, 3])
Objectref({ id: 1 })
Nullref(null)

Reactivity Behavior

OperationReactive
count.value = 10Yes
count.value++Yes
user.value.name = 'Bob'Yes
user.value = {...}Yes
items.value.push(x)Yes
state.count.value = 10Yes

Best Practices

✅ Do This:

<!-- Use ref for all state -->
const count = ref(0)                                           <!-- ✅ -->

<!-- Use .value in the script -->
count.value++                                                  <!-- ✅ -->

<!-- Use the ref directly in the template -->
{{ count }}                                                    <!-- ✅ -->

<!-- Return refs from composables -->
return { count, increment }                                    <!-- ✅ -->

<!-- Use toRefs to destructure reactive objects -->
const { name, age } = toRefs(user)                             <!-- ✅ -->

<!-- Use shallowRef for large data structures -->
const bigData = shallowRef(loadLargeData())                    <!-- ✅ -->

<!-- Use computed for derived values -->
const doubled = computed(() => count.value * 2)                <!-- ✅ -->

❌ Don’t Do This:

<!-- Don't forget .value in the script -->
count++  // does not update the ref                             <!-- ❌ -->

<!-- Don't use .value in the template -->
{{ count.value }}  // unnecessary                                <!-- ⚠️ -->

<!-- Don't destructure a reactive object without toRefs -->
const { name } = user  // loses reactivity                       <!-- ❌ -->

<!-- Don't nest refs in plain objects -->
const state = { count: ref(0) }  // not auto-unwrapped           <!-- ⚠️ -->

<!-- Don't use ref for values that do not change -->
const PI = ref(3.14)  // use a plain constant                    <!-- ⚠️ -->

<!-- Don't use reactive for primitives -->
const count = reactive(0)  // error                              <!-- ❌ -->

<!-- Don't mutate a ref's value from a different component -->
props.count.value  // props are read-only                       <!-- ❌ -->

Common Pitfalls

PitfallWhy It HappensFix
Value does not updateMissing .valueUse count.value
Template shows ref objectUsed .value in templateRemove .value
Destructured state loses reactivityUsed plain destructuringUse toRefs
Nested ref not unwrappedRef inside plain objectUse .value
Re-render not triggeredMutated nested in shallowRefUse ref or triggerRef
Props mutatedRef from propsEmit an event

Real-World Examples

1. Counter

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

2. Form Input

<script setup>
import { ref } from 'vue'
const name = ref('')
</script>

<template>
  <input v-model="name">
  <p>{{ name }}</p>
</template>

3. Todo List

<script setup>
import { ref } from 'vue'
const todos = ref([])
const newTodo = ref('')

function addTodo() {
  todos.value.push({ id: Date.now(), text: newTodo.value })
  newTodo.value = ''
}
</script>

4. User Object

<script setup>
import { ref } from 'vue'
const user = ref({ name: 'Alice', email: 'alice@example.com' })
</script>

<template>
  <p>{{ user.name }}</p>
  <p>{{ user.email }}</p>
</template>

5. Loading State

<script setup>
import { ref, onMounted } from 'vue'
const loading = ref(true)
const data = ref(null)

onMounted(async () => {
  const res = await fetch('/api/data')
  data.value = await res.json()
  loading.value = false
})
</script>

6. Composable

export function useToggle(initial = false) {
  const value = ref(initial)
  const toggle = () => value.value = !value.value
  return { value, toggle }
}

7. Computed from Refs

<script setup>
import { ref, computed } from 'vue'
const items = ref([1, 2, 3])
const total = computed(() => items.value.reduce((a, b) => a + b, 0))
</script>

8. toRefs

<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state)
</script>

9. Shallow Ref for Large Data

<script setup>
import { shallowRef } from 'vue'
const chartData = shallowRef(loadChartData())
</script>

10. Trigger Ref

<script setup>
import { shallowRef, triggerRef } from 'vue'
const list = shallowRef([1, 2, 3])

function addItem() {
  list.value.push(4)
  triggerRef(list)
}
</script>

Visual

Ref Structure

┌─────────────────────────────────────────────────────────────┐
│  const count = ref(0)                                       │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Ref object                                         │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │  .value: 0                                  │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  │                                                     │    │
│  │  Proxy intercepts reads and writes to .value        │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Script: count.value                                        │
│  Template: count (auto-unwrapped)                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Script vs Template

┌─────────────────────────────────────────────────────────────┐
│  <script setup>                                             │
│  const count = ref(0)                                       │
│  const doubled = computed(() => count.value * 2)            │
│                                                             │
│  console.log(count.value)      // 0                         │
│  count.value++                 // write                     │
│  </script>                                                  │
│                                                             │
│  <template>                                                 │
│    {{ count }}       <!-- count.value -->                   │
│    {{ doubled }}     <!-- doubled.value -->                 │
│    <button @click="count++">  <!-- count.value++ -->        │
│  </template>                                                │
│                                                             │
│  .value in script. No .value in template.                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Reactivity Trigger

┌─────────────────────────────────────────────────────────────┐
│  const count = ref(0)                                       │
│                                                             │
│  Template reads count.value                                 │
│    │                                                        │
│    ▼                                                        │
│  Dependency tracked: this component depends on count        │
│    │                                                        │
│    ▼                                                        │
│  count.value = 10                                           │
│    │                                                        │
│    ▼                                                        │
│  Proxy intercepts the write                                 │
│    │                                                        │
│    ▼                                                        │
│  Notify dependents                                          │
│    │                                                        │
│    ▼                                                        │
│  Component re-renders                                       │
│    │                                                        │
│    ▼                                                        │
│  DOM updates to show 10                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Nested Ref Not Unwrapped

┌─────────────────────────────────────────────────────────────┐
│  const state = { count: ref(0) }                            │
│                                                             │
│  Top-level ref:                                             │
│  const count = ref(0)                                       │
│  Template: {{ count }}          → 0                         │
│                                                             │
│  Nested ref:                                                │
│  const state = { count: ref(0) }                            │
│  Template: {{ state.count }}    → [object Object]           │
│  Template: {{ state.count.value }} → 0                      │
│                                                             │
│  Auto-unwrapping applies to top-level refs only.            │
│  Nested refs require explicit .value.                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Functionref(initialValue)
ReturnsReactive ref object
Access in script.value
Access in templateAuto-unwrapped
Value typesAny
ReactivityOn .value write
ComparisonObject.is
Shallow variantshallowRef
Value extractionunref
Ref checkisRef
Object to refstoRefs
Property to reftoRef
Manual triggertriggerRef

Key takeaways:

  • ref() creates a reactive reference to a value. The value is stored in the .value property. In the script, it is accessed through .value. In the template, it is auto-unwrapped.
  • A ref can hold any value type. Primitives, objects, arrays, and other refs. The .value access is uniform, and the reactivity is uniform.
  • The reactivity triggers on writes to .value. Vue compares the new value to the old value with Object.is and re-renders the components that depend on the ref if the value changed.
  • Auto-unwrapping applies only to top-level refs in the template. A ref nested inside a plain object is not unwrapped. This is why top-level refs are preferred.
  • shallowRef() creates a ref that is only reactive at the top level. Mutating a nested property does not trigger a re-render. Use it for large data structures where deep reactivity is unnecessary.
  • toRefs() preserves reactivity when destructuring a reactive object. Without it, the destructured properties are plain values that do not update.
  • Composables return refs. The component that uses the composable reads and writes the refs. The reactivity travels through them, so the component re-renders when the composable’s state changes.

Remember: ref() is the primary tool for declaring reactive state in a Vue component. It wraps a value in a reactive object with a .value property. The script accesses the value through .value; the template auto-unwraps it. The reactivity triggers on writes, and the components that read the ref re-render. Use ref() for primitives and for objects that are replaced or deeply mutated. Use shallowRef() for large data where deep reactivity is expensive. Use toRefs() to destructure reactive objects without losing reactivity. And remember that the .value is the mechanism: the script is plain JavaScript, and the template is a compiled render function that accesses the value for you.



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!