iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…