Vue.js 50 🟢 Composable Conventions: Input Argument Normalization with toValue()
A composable that accepts a value has to decide what it accepts. A plain number, a ref, a computed, or a getter function: all four are reasonable, and all four appear in real code. A composable that only accepts one of them is rigid, and a composable that requires the caller to unwrap is annoying. The toValue() function is the utility that solves this. It normalizes a value, a ref, or a getter to a plain value, so the composable can accept any of them without branching. The convention is to call toValue() on the argument at the point of use, and to re-evaluate it inside a watch or a watchEffect so that reactive inputs stay reactive.
Key point: toValue() unwraps a ref to its value, calls a getter to produce a value, and returns a plain value unchanged. It does not create reactivity. If the argument is a reactive source, the composable must read it inside a reactive effect (watch, watchEffect, computed) for the reactivity to propagate.
Why toValue exists
The argument problem. A composable like useFetch(url) may be called with a string, a ref<string>, a computed<string>, or a getter () => string. Each form is idiomatic in a different context. A string is used for a fixed URL. A ref is used when the URL changes with state. A computed is used when the URL is derived. A getter is used when the caller wants to defer the evaluation.
The branching problem. Without toValue, the composable must check the type of the argument on every read: typeof url === 'function' ? url() : isRef(url) ? url.value : url. The check is repetitive and error-prone, and it must be repeated for every argument.
The unwrap problem. A composable that only accepts a ref forces the caller to wrap a plain value in a ref. A composable that only accepts a plain value loses reactivity. Neither is acceptable.
The getter convention. A getter is a function that returns a value. In the Composition API, a getter is the standard way to pass a reactive value without creating a ref. A computed is a getter with caching. A watch source is a getter. The toValue function accepts all of them.
The MaybeRefOrGetter type. The TypeScript type for the union is MaybeRefOrGetter<T>, which is T | Ref<T> | (() => T). The type documents the accepted forms, and toValue is the function that resolves the union.
The reactivity problem. toValue does not subscribe to the source. It reads the current value and returns it. To react to changes, the composable must read the value inside a watch, a watchEffect, or a computed. The combination of toValue and a reactive effect is the pattern.
The compatibility problem. The toValue function was introduced in Vue 3.3. Before that, the pattern was unref for refs and a manual getter check. The unref function is still available, but it does not handle getters. The toValue function handles all three forms and is the modern utility.
a. The toValue function
The toValue function is imported from vue. It takes one argument and returns the resolved value.
The signature
function toValue<T>(source: MaybeRefOrGetter<T>): T
The source is a value, a ref, or a getter. The return is the plain value.
The plain value
A plain value is returned unchanged.
import { toValue } from 'vue';
const result = toValue(42);
console.log(result); // 42
The ref
A ref is unwrapped to its value.
import { ref, toValue } from 'vue';
const count = ref(5);
const result = toValue(count);
console.log(result); // 5
The toValue function is equivalent to unref for a ref.
The getter
A getter is called and its return value is returned.
import { toValue } from 'vue';
const result = toValue(() => 42);
console.log(result); // 42
The getter is invoked on each call to toValue.
The computed
A computed is a ref, and toValue unwraps it.
import { ref, computed, toValue } from 'vue';
const count = ref(2);
const doubled = computed(() => count.value * 2);
const result = toValue(doubled);
console.log(result); // 4
The function that is not a getter
A function that takes arguments is not a getter, and toValue calls it with no arguments.
import { toValue } from 'vue';
function add(a, b) { return a + b; }
const result = toValue(add);
console.log(result); // NaN (add is called with no arguments)
The toValue function treats every function as a getter. A function that expects arguments does not work as an input.
The comparison with unref
The unref function unwraps a ref but does not call a getter.
import { ref, unref, toValue } from 'vue';
const count = ref(5);
console.log(unref(count)); // 5
console.log(toValue(count)); // 5
console.log(unref(() => 5)); // the function itself
console.log(toValue(() => 5)); // 5
The toValue function is the superset. It handles the getter case that unref does not.
The type
The TypeScript signature accepts MaybeRefOrGetter<T> and returns T.
import { toValue, type MaybeRefOrGetter } from 'vue';
function useThing(value: MaybeRefOrGetter<number>) {
const resolved = toValue(value);
return resolved * 2;
}
The MaybeRefOrGetter type documents the three accepted forms.
b. The composable pattern
The pattern is to accept MaybeRefOrGetter<T> arguments and to call toValue inside a reactive effect. The pattern is the convention for every composable that takes a reactive input.
The basic composable
import { watchEffect, toValue } from 'vue';
export function useLogger(source) {
watchEffect(() => {
console.log(toValue(source));
});
}
The watchEffect re-runs whenever the reactive dependencies of source change. The toValue resolves the current value.
The useFetch with a reactive URL
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
watchEffect(async () => {
const resolvedUrl = toValue(url);
try {
const response = await fetch(resolvedUrl);
data.value = await response.json();
} catch (err) {
error.value = err;
}
});
return { data, error };
}
The composable accepts a string, a ref, a computed, or a getter. The watchEffect re-fetches when the URL changes.
The useIntersectionObserver
import { ref, watchEffect, toValue } from 'vue';
export function useIntersectionObserver(target) {
const isVisible = ref(false);
watchEffect((onCleanup) => {
const element = toValue(target);
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
isVisible.value = entry.isIntersecting;
});
observer.observe(element);
onCleanup(() => observer.disconnect());
});
return { isVisible };
}
The target is a ref to a DOM element. The toValue resolves it inside the effect.
The useDebouncedRef
import { ref, watch, toValue } from 'vue';
export function useDebouncedRef(source, delay = 300) {
const debounced = ref(toValue(source));
let timer = null;
watch(
() => toValue(source),
(newValue) => {
clearTimeout(timer);
timer = setTimeout(() => {
debounced.value = newValue;
}, delay);
}
);
return debounced;
}
The watch source is a getter that calls toValue. The reactivity flows from the source to the debounced ref.
The useLocalStorage
import { ref, watch, toValue } from 'vue';
export function useLocalStorage(key, defaultValue) {
const value = ref(localStorage.getItem(toValue(key)) ?? toValue(defaultValue));
watch(value, (v) => {
localStorage.setItem(toValue(key), JSON.stringify(v));
}, { deep: true });
return value;
}
The key and the defaultValue accept any of the three forms. The toValue resolves them at the point of use.
The useMediaQuery
import { ref, watchEffect, toValue } from 'vue';
export function useMediaQuery(query) {
const matches = ref(false);
watchEffect((onCleanup) => {
const mql = window.matchMedia(toValue(query));
matches.value = mql.matches;
const handler = (e) => { matches.value = e.matches; };
mql.addEventListener('change', handler);
onCleanup(() => mql.removeEventListener('change', handler));
});
return matches;
}
The query accepts a string, a ref, or a getter. The watchEffect re-evaluates when the query changes.
The argument type annotation
In TypeScript, the argument is typed with MaybeRefOrGetter<T>.
import { type MaybeRefOrGetter, toValue } from 'vue';
export function useFormattedDate(
date: MaybeRefOrGetter<Date>,
locale: MaybeRefOrGetter<string> = 'en-US'
) {
const formatted = computed(() => {
return toValue(date).toLocaleDateString(toValue(locale));
});
return { formatted };
}
The type documents the accepted forms, and toValue resolves them.
The default value
A default value is a plain value. It does not need to be a MaybeRefOrGetter.
export function useDebouncedRef(
source: MaybeRefOrGetter<string>,
delay = 300
) {
// ...
}
The delay is a plain number, and the source is MaybeRefOrGetter<string>.
c. The reactivity rules
The toValue function reads the current value. The reactivity is what makes the composable re-evaluate when the value changes, and the reactive effect is what provides it.
The reactive effect
A watch, a watchEffect, or a computed tracks the reactive dependencies of the function passed to it. The toValue call inside the effect reads the ref or calls the getter, and the effect re-runs when the source changes.
// ❌ No reactivity
const resolved = toValue(source);
console.log(resolved);
// ✅ Reactive
watchEffect(() => {
const resolved = toValue(source);
console.log(resolved);
});
The first form reads the value once. The second form re-reads it whenever the source changes.
The watch source
A watch accepts a getter as its source. The getter calls toValue, and the watch re-runs when the resolved value changes.
watch(
() => toValue(source),
(newValue, oldValue) => {
// ...
}
);
The computed
A computed is a getter with caching. The toValue call inside the getter reads the source, and the computed re-evaluates when the source changes.
const resolved = computed(() => toValue(source));
The unwrapped read
A ref passed as an argument is unwrapped by toValue. The caller can pass the ref directly, and the composable reads its .value through toValue.
const url = ref('/api/data');
useFetch(url); // The composable calls toValue(url) inside an effect.
The caller does not need to unwrap the ref.
The getter for deferred reads
A getter defers the evaluation until the composable calls it. The caller can pass a getter that reads from a store, computes a value, or reads a DOM property.
useFetch(() => `/api/users/${userId.value}`);
The getter reads userId.value when the composable evaluates it inside the effect.
The reactive object caveat
A reactive object is not a MaybeRefOrGetter. The toValue function does not handle it. The composable that accepts a reactive object must read the property inside the effect.
const state = reactive({ url: '/api/data' });
// ✅ Read the property inside the effect.
watchEffect(() => {
const resolved = state.url;
// ...
});
The computed versus the getter
A computed caches the value and tracks its dependencies. A getter re-evaluates on every call. The computed is preferred when the value is expensive to compute or read by multiple consumers. The getter is preferred when the value is cheap or when the caller wants to avoid the cache.
The pattern table
| Input | toValue returns | Reactive effect required |
|---|---|---|
| Plain value | The value | No |
ref | .value | Yes |
computed | .value | Yes |
| Getter | The return value | Yes |
reactive object | Not supported | N/A |
Complete Example Session
// ============================================
// PART 1: THE BASIC TOVALUE
// ============================================
import { ref, computed, toValue } from 'vue';
console.log(toValue(42)); // 42
console.log(toValue(ref(5))); // 5
console.log(toValue(() => 42)); // 42
console.log(toValue(computed(() => 7))); // 7
// ============================================
// PART 2: THE USELOGGER
// ============================================
import { watchEffect, toValue } from 'vue';
export function useLogger(source) {
watchEffect(() => {
console.log(toValue(source));
});
}
// ============================================
// PART 3: THE USEFETCH WITH A REACTIVE URL
// ============================================
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
watchEffect(async () => {
const resolvedUrl = toValue(url);
try {
const response = await fetch(resolvedUrl);
data.value = await response.json();
} catch (err) {
error.value = err;
}
});
return { data, error };
}
// ============================================
// PART 4: THE USEDEBOUNCEDREF
// ============================================
import { ref, watch, toValue } from 'vue';
export function useDebouncedRef(source, delay = 300) {
const debounced = ref(toValue(source));
let timer = null;
watch(
() => toValue(source),
(newValue) => {
clearTimeout(timer);
timer = setTimeout(() => {
debounced.value = newValue;
}, delay);
}
);
return debounced;
}
// ============================================
// PART 5: THE USEMEDIAQUERY
// ============================================
import { ref, watchEffect, toValue } from 'vue';
export function useMediaQuery(query) {
const matches = ref(false);
watchEffect((onCleanup) => {
const mql = window.matchMedia(toValue(query));
matches.value = mql.matches;
const handler = (e) => { matches.value = e.matches; };
mql.addEventListener('change', handler);
onCleanup(() => mql.removeEventListener('change', handler));
});
return matches;
}
// ============================================
// PART 6: THE TYPESCRIPT VERSION
// ============================================
import { type MaybeRefOrGetter, toValue, computed } from 'vue';
export function useFormattedDate(
date: MaybeRefOrGetter<Date>,
locale: MaybeRefOrGetter<string> = 'en-US'
) {
return computed(() => {
return toValue(date).toLocaleDateString(toValue(locale));
});
}
// ============================================
// PART 7: THE COMPONENT USAGE
// ============================================
// App.vue
<script setup>
import { ref } from 'vue';
import { useFetch } from '@/composables/useFetch';
const userId = ref(42);
const url = computed(() => `/api/users/${userId.value}`);
const { data, error } = useFetch(url);
const query = ref('(max-width: 768px)');
const isMobile = useMediaQuery(query);
</script>
<template>
<p v-if="error">{{ error.message }}</p>
<pre v-else>{{ data }}</pre>
<p v-if="isMobile">Mobile layout</p>
</template>
// ============================================
// PART 8: THE REACTIVITY RULES
// ============================================
// ❌ No reactivity
const resolved = toValue(url);
// Reads once.
// ✅ Reactive
watchEffect(() => {
const resolved = toValue(url);
// Reads and re-reads on change.
});
// ✅ Reactive with watch
watch(
() => toValue(url),
(newValue) => { /* ... */ }
);
// ✅ Reactive with computed
const resolved = computed(() => toValue(url));
// ============================================
// PART 9: THE COMPARISON WITH UNREF
// ============================================
import { ref, unref, toValue } from 'vue';
unref(ref(5)); // 5
unref(() => 5); // the function
toValue(ref(5)); // 5
toValue(() => 5); // 5
// toValue handles getters. unref does not.
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. Accept MaybeRefOrGetter<T> for reactive inputs.
// 2. Call toValue inside a reactive effect.
// 3. Use watch, watchEffect, or computed for the effect.
// 4. Do not call toValue outside an effect for reactive inputs.
// 5. Use plain values for non-reactive arguments.
// 6. Document the accepted forms with MaybeRefOrGetter.
// 7. Use toValue instead of unref for the getter support.
The ten parts showed the basic toValue, useLogger, useFetch, useDebouncedRef, useMediaQuery, the TypeScript version, the component usage, the reactivity rules, the comparison with unref, and the complete pattern.
Quick Reference
The toValue Function
| Input | Return |
|---|---|
| Plain value | The value |
ref | .value |
computed | .value |
| Getter | The return value |
reactive | Not supported |
The Types
| Type | Meaning |
|---|---|
MaybeRef<T> | T | Ref<T> |
MaybeRefOrGetter<T> | T | Ref<T> | (() => T) |
toValue<T>(source) | Returns T |
The Reactive Effects
| Effect | Purpose |
|---|---|
watchEffect | Re-run on any dependency change |
watch | Re-run on a specific source change |
computed | Derive a cached value |
The Comparison with unref
| Input | unref | toValue |
|---|---|---|
| Plain value | The value | The value |
ref | .value | .value |
computed | .value | .value |
| Getter | The function | The return value |
The Input Forms
| Form | Example |
|---|---|
| Plain value | useFetch('/api/data') |
ref | useFetch(urlRef) |
computed | useFetch(computed(() => ...)) |
| Getter | useFetch(() => '/api/' + id.value) |
Best Practices
✅ Do This:
// Accept MaybeRefOrGetter for reactive inputs.
export function useFetch(url: MaybeRefOrGetter<string>) { } // ✅
// Call toValue inside a reactive effect.
watchEffect(() => {
const resolved = toValue(url);
}); // ✅
// Use watch with a getter source.
watch(
() => toValue(source),
(newValue) => { /* ... */ }
); // ✅
// Use computed for derived values.
const resolved = computed(() => toValue(source)); // ✅
// Use plain values for non-reactive arguments.
export function useDebouncedRef(source, delay = 300) { } // ✅
// Document the accepted forms with the type.
function useThing(value: MaybeRefOrGetter<number>) { } // ✅
❌ Don’t Do This:
// Don't call toValue outside an effect for a reactive input.
const resolved = toValue(url); // ❌ Reads once
// Don't use unref when a getter is possible.
const resolved = unref(source); // ❌ Does not call getters
// Don't pass a reactive object.
useFetch(state); // ❌ toValue does not support it
// Don't expect toValue to create reactivity.
const resolved = toValue(source); // ❌ Not reactive
// Don't forget the type annotation.
export function useFetch(url) { } // ⚠️ No MaybeRefOrGetter
// Don't pass a function that takes arguments.
useFetch(add); // ❌ add is called with no arguments
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| No reactivity | toValue outside an effect | Use watchEffect |
| Getter not called | Used unref | Use toValue |
| Reactive object fails | toValue does not support it | Read the property in the effect |
| Function called with no args | Treated as a getter | Use a plain value |
| Lost reactivity | Destructured the result | Read inside the effect |
| Type error | No MaybeRefOrGetter | Annotate the argument |
| Cached value wrong | computed used with an unstable getter | Use a stable getter |
Real-World Examples
1. Plain Value
toValue(42); // 42
2. Ref
toValue(ref(5)); // 5
3. Getter
toValue(() => 42); // 42
4. Computed
toValue(computed(() => 7)); // 7
5. useFetch
useFetch(() => `/api/users/${id.value}`);
6. useDebouncedRef
watch(() => toValue(source), (v) => { /* ... */ });
7. useMediaQuery
const mql = window.matchMedia(toValue(query));
8. The Type
function useThing(value: MaybeRefOrGetter<number>) { }
9. The Comparison
unref(() => 5); // the function
toValue(() => 5); // 5
10. The Complete Pattern
watchEffect(() => {
const resolved = toValue(source);
// Use the resolved value.
});
Visual
The toValue Resolution
┌────────────────────────────────────────────────────────────┐
│ THE toValue RESOLUTION │
│ │
│ INPUT OUTPUT │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 42 │──toValue──▶│ 42 │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ ref(5) │──toValue──▶│ 5 │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ computed(()=>7) │──toValue──▶│ 7 │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ () => 42 │──toValue──▶│ 42 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ The plain value passes through. │
│ The ref is unwrapped. │
│ The getter is called. │
└────────────────────────────────────────────────────────────┘
The Reactivity Requirement
┌────────────────────────────────────────────────────────────┐
│ THE REACTIVITY REQUIREMENT │
│ │
│ ❌ OUTSIDE AN EFFECT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ const resolved = toValue(source); │ │
│ │ → reads once │ │
│ │ → no subscription │ │
│ │ → the value never updates │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ✅ INSIDE AN EFFECT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ watchEffect(() => { │ │
│ │ const resolved = toValue(source); │ │
│ │ }); │ │
│ │ → reads and subscribes │ │
│ │ → re-runs when the source changes │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ toValue reads. The effect subscribes. │
└────────────────────────────────────────────────────────────┘
The MaybeRefOrGetter Type
┌────────────────────────────────────────────────────────────┐
│ MaybeRefOrGetter<T> │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ type MaybeRefOrGetter<T> = │ │
│ │ | T ← plain value │ │
│ │ | Ref<T> ← a ref │ │
│ │ | (() => T) ← a getter │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The type documents the accepted forms. │
│ toValue resolves the union. │
│ │
│ Usage: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ function useFetch(url: MaybeRefOrGetter<string>) │ │
│ │ // The caller can pass a string, a ref, or a getter│ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The unref vs toValue
┌────────────────────────────────────────────────────────────┐
│ unref vs toValue │
│ │
│ unref │
│ ┌──────────────────────────────────────────────────┐ │
│ │ unref(ref(5)) → 5 │ │
│ │ unref(() => 5) → the function │ │
│ │ unref(42) → 42 │ │
│ │ │ │
│ │ Handles plain values and refs. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ toValue │
│ ┌──────────────────────────────────────────────────┐ │
│ │ toValue(ref(5)) → 5 │ │
│ │ toValue(() => 5) → 5 │ │
│ │ toValue(42) → 42 │ │
│ │ │ │
│ │ Handles plain values, refs, AND getters. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ toValue is the superset. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
toValue | Resolves a value, ref, or getter |
MaybeRefOrGetter<T> | T | Ref<T> | (() => T) |
| Plain value | Returned unchanged |
ref | Unwrapped to .value |
computed | Unwrapped to .value |
| Getter | Called and returned |
unref | Handles values and refs, not getters |
| Reactive effect | Required for reactivity |
watchEffect | Re-runs on any dependency change |
watch | Re-runs on a source change |
Key takeaways:
toValuenormalizes a value, a ref, or a getter to a plain value. It handles the three forms that a composable might receive, and it is the modern replacement forunrefwhen getters are possible.- The
MaybeRefOrGetter<T>type documents the accepted forms. The union isT | Ref<T> | (() => T), and the type annotation tells the caller what they can pass. toValuedoes not create reactivity. It reads the current value. The reactivity comes from the reactive effect that contains the call. ThewatchEffect, thewatch, and thecomputedare the effects that subscribe.- The
watchsource is a getter. The pattern iswatch(() => toValue(source), (newValue) => { ... }). The getter reads the source, and the watch subscribes to its dependencies. - The
computedis the derived value. The pattern iscomputed(() => toValue(source)). The computed caches the result and re-evaluates when the source changes. - A
reactiveobject is not aMaybeRefOrGetter. ThetoValuefunction does not support it. The composable that accepts areactiveobject must read the property inside the effect. - A function that takes arguments is not a getter. The
toValuefunction calls every function with no arguments. A function that expects arguments producesNaNor an error. - The composable accepts any of the three forms. The caller passes a string, a
ref, acomputed, or a getter, and the composable resolves it withtoValue. The result is a flexible API and a predictable behavior.
Remember: The toValue function is the composable’s utility for input argument normalization. It resolves a plain value, a ref, a computed, or a getter to a plain value, and it is the superset of unref that handles the getter case. The composable accepts MaybeRefOrGetter<T> arguments and calls toValue inside a reactive effect, so the reactivity propagates from the source to the composable’s internal state. The watchEffect, the watch, and the computed are the effects that subscribe. The result is a composable that accepts any of the common input forms without branching, and a set of conventions that make the Composition API composable.
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!