| |

Vue.js 50 🟢 Composable Conventions: Input Argument Normalization with toValue()

⏱️ Estimated read: 16 min | |

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

InputtoValue returnsReactive effect required
Plain valueThe valueNo
ref.valueYes
computed.valueYes
GetterThe return valueYes
reactive objectNot supportedN/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

InputReturn
Plain valueThe value
ref.value
computed.value
GetterThe return value
reactiveNot supported

The Types

TypeMeaning
MaybeRef<T>T | Ref<T>
MaybeRefOrGetter<T>T | Ref<T> | (() => T)
toValue<T>(source)Returns T

The Reactive Effects

EffectPurpose
watchEffectRe-run on any dependency change
watchRe-run on a specific source change
computedDerive a cached value

The Comparison with unref

InputunreftoValue
Plain valueThe valueThe value
ref.value.value
computed.value.value
GetterThe functionThe return value

The Input Forms

FormExample
Plain valueuseFetch('/api/data')
refuseFetch(urlRef)
computeduseFetch(computed(() => ...))
GetteruseFetch(() => '/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

PitfallWhy It HappensFix
No reactivitytoValue outside an effectUse watchEffect
Getter not calledUsed unrefUse toValue
Reactive object failstoValue does not support itRead the property in the effect
Function called with no argsTreated as a getterUse a plain value
Lost reactivityDestructured the resultRead inside the effect
Type errorNo MaybeRefOrGetterAnnotate the argument
Cached value wrongcomputed used with an unstable getterUse 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

ItemValue
toValueResolves a value, ref, or getter
MaybeRefOrGetter<T>T | Ref<T> | (() => T)
Plain valueReturned unchanged
refUnwrapped to .value
computedUnwrapped to .value
GetterCalled and returned
unrefHandles values and refs, not getters
Reactive effectRequired for reactivity
watchEffectRe-runs on any dependency change
watchRe-runs on a source change

Key takeaways:

  • toValue normalizes 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 for unref when getters are possible.
  • The MaybeRefOrGetter<T> type documents the accepted forms. The union is T | Ref<T> | (() => T), and the type annotation tells the caller what they can pass.
  • toValue does not create reactivity. It reads the current value. The reactivity comes from the reactive effect that contains the call. The watchEffect, the watch, and the computed are the effects that subscribe.
  • The watch source is a getter. The pattern is watch(() => toValue(source), (newValue) => { ... }). The getter reads the source, and the watch subscribes to its dependencies.
  • The computed is the derived value. The pattern is computed(() => toValue(source)). The computed caches the result and re-evaluates when the source changes.
  • A reactive object is not a MaybeRefOrGetter. The toValue function does not support it. The composable that accepts a reactive object must read the property inside the effect.
  • A function that takes arguments is not a getter. The toValue function calls every function with no arguments. A function that expects arguments produces NaN or an error.
  • The composable accepts any of the three forms. The caller passes a string, a ref, a computed, or a getter, and the composable resolves it with toValue. 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.


Was this helpful?


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!