ref()
ref wraps a single value in a reactive container. It’s the lowest-level reactive primitive — reactive, computed, and the rest of Vue’s reactivity all build on the same idea. Master .value, ref-unwrapping rules, and shallow refs and Vue stops surprising you.
ref, unwrap, shallowRef, template refs
EXAMPLE
<script setup lang="ts">
import { ref, shallowRef, triggerRef, isRef, unref, toRef, toValue, onMounted, computed, watch } from 'vue';
// 1) Create a ref
const count = ref(0); // ref<number>
const name = ref('Mara');
const todos = ref<Todo[]>([]);
const user = ref<User | null>(null);
console.log(count.value); // 0 — .value in JS
count.value++;
// 2) Template auto-unwrapping — no .value in templates
// <p>{{ count }} {{ name }}</p> — works
// <p>{{ count.value }}</p> — also works but redundant
// Auto-unwrap ONLY in TOP-LEVEL template expressions, NOT inside objects:
// <p>{{ { x: count }.x }}</p> — count is the ref, NOT unwrapped
// 3) Refs in reactive objects — auto-unwrapped
import { reactive } from 'vue';
const state = reactive({ count });
console.log(state.count); // 0 — auto-unwrapped
state.count++; // updates the same ref
console.log(count.value); // 1
// 4) Composition with computed + watch
const doubled = computed(() => count.value * 2);
watch(count, (n, old) => console.log(`${old} → ${n}`));
watch(() => state.count, (n) => console.log(n)); // watch a getter
// 5) Template refs — references to DOM elements
const inputEl = ref<HTMLInputElement | null>(null);
onMounted(() => inputEl.value?.focus());
</script>
<template>
<input ref="inputEl" />
</template>
// 6) Template refs on components — refer to the component instance
<script setup lang="ts">
import { ref } from 'vue';
import Child from './Child.vue';
const childRef = ref<InstanceType<typeof Child> | null>(null);
function reset() { childRef.value?.reset(); }
</script>
<template>
<Child ref="childRef" />
<button @click="reset">Reset child</button>
</template>
// 7) defineExpose — control what parent sees on a child instance
<script setup lang="ts">
import { ref } from 'vue';
const counter = ref(0);
function reset() { counter.value = 0; }
defineExpose({ reset });
</script>
// 8) shallowRef — opt out of deep reactivity (perf)
const big = shallowRef({ rows: hugeArray });
big.value.rows.push(newRow); // does NOT trigger updates
big.value = { rows: [...big.value.rows, newRow] }; // assignment triggers
triggerRef(big); // manual trigger if you mutate in place
// Best for: 3rd-party objects (ECharts, Mapbox), huge collections you control manually.
// 9) Helpers
isRef(count); // true
isRef(5); // false
unref(count); // 5 — same as count.value (or value itself if not a ref)
unref(5); // 5
toValue(count); // unref + getter — unwraps refs OR calls a getter
// 10) toRef — extract a reactive reference into a ref
import { toRef } from 'vue';
const state = reactive({ name: 'Mara', age: 30 });
const nameRef = toRef(state, 'name');
nameRef.value = 'Sam'; // updates state.name + vice versa
// Or build a getter-based ref (Vue 3.3+)
const nameRef2 = toRef(() => state.name);
// Read-only; tracks changes; useful for passing into composables that expect refs.
// 11) Custom refs — control how triggers happen
import { customRef } from 'vue';
function useDebouncedRef<T>(initial: T, delay = 200) {
let timer: number;
return customRef<T>((track, trigger) => ({
get() { track(); return initial; },
set(v) {
clearTimeout(timer);
timer = window.setTimeout(() => { initial = v; trigger(); }, delay);
},
}));
}
const search = useDebouncedRef('', 300);
// <input v-model="search" /> — updates debounced
// 12) Refs in destructuring (loses reactivity!)
const { count: c } = state; // c is a primitive, not a ref
// Fix with toRefs:
import { toRefs } from 'vue';
const { count: c } = toRefs(state); // c is a ref<number>
// 13) When to use ref vs reactive
// • ref — single primitive or object you want to replace whole (avatars, settings)
// • reactive — object you mutate field-by-field (user profile, form state)
// • Most teams use ref EVERYWHERE for consistency. Both work.
// 14) Common bugs
// • Forgot .value in JS — typeof count !== 'number'; arithmetic returns NaN
// • Wrote .value in templates — works, but unnecessary noise
// • Destructured a reactive() — lost reactivity; use toRefs
// • shallowRef with deep mutation expecting reactivity — call triggerRef or assign whole new value
// • Template ref with v-if — null when hidden; check before use
// • Passing ref into a non-Vue function — pass count.value or use toValue
// • Storing a non-reactive 3rd-party object in ref — internal mutations not seen; use shallowRef
// • Using ref for a derived value — use computed (cached + reactive deps)
Why it matters
ref wraps a value with a reactive container; you read with .value in JS and the template unwraps automatically. Reach for shallowRef with huge collections or 3rd-party objects you don’t want Vue to traverse, customRef for debouncing or other custom triggers, and toRefs when destructuring a reactive object so reactivity survives.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // unwrap with .value in JS
// templates auto-unwrap.
Try it Yourself »
Discussion
Loading…