shallowRef() signature and behavior
shallowRef creates a shallow version of ref. Its type signature is: function shallowRef<T>(value: T): ShallowRef<T> where ShallowRef<T> has a value property of type T. Unlike ref(), the inner value is stored and exposed as-is without deep reactivity. Only the .value access is reactive, not mutations to nested properties.
shallowRef example: what triggers change
When using shallowRef, mutating nested properties directly (state.value.count = 2) does NOT trigger change. Only reassigning the entire .value property (state.value = { count: 2 }) triggers change.
triggerRef() signature and purpose
triggerRef has the signature: function triggerRef(ref: ShallowRef): void. It forces trigger effects that depend on a shallow ref, typically used after making deep mutations to the inner value of a shallow ref.
triggerRef example
Example: const shallow = shallowRef({ greet: 'Hello, world' }); watchEffect(() => { console.log(shallow.value.greet) }); shallow.value.greet = 'Hello, universe'; // Won't trigger the effect because ref is shallow; triggerRef(shallow); // Now it logs 'Hello, universe'
customRef() signature and factory function
customRef creates a customized ref with explicit control over dependency tracking and updates. Signature: function customRef<T>(factory: CustomRefFactory<T>): Ref<T> where CustomRefFactory<T> = (track: () => void, trigger: () => void) => { get: () => T, set: (value: T) => void }. The factory receives track and trigger functions and must return an object with get and set methods.
customRef get and set methods: when to call track and trigger
In customRef, track() should generally be called inside get(), and trigger() should be called inside set(). However, you have full control over when and whether they should be called.
customRef example: debounced ref
Example of a debounced ref that only updates after a timeout: import { customRef } from 'vue'; export function useDebouncedRef(value, delay = 200) { let timeout; return customRef((track, trigger) => { return { get() { track(); return value }, set(newValue) { clearTimeout(timeout); timeout = setTimeout(() => { value = newValue; trigger(); }, delay) } } }) }
customRef warning: getter return value and props
When using customRef, be cautious about the return value of its getter, especially when generating new object datatypes each time. If passed as a prop to a child component, the new object on each reevaluation can cause identity differences and unexpected reactive behavior in the child component while parent dependencies may not trigger.
shallowReactive() signature and behavior
shallowReactive has the signature: function shallowReactive<T extends object>(target: T): T. It is a shallow version of reactive() with no deep conversion - only root-level properties are reactive. Property values are stored as-is, and properties with ref values will NOT be automatically unwrapped.
shallowReactive example: nested properties not reactive
Example: const state = shallowReactive({ foo: 1, nested: { bar: 2 } }); state.foo++; // reactive; isReactive(state.nested) // false; state.nested.bar++; // NOT reactive
shallowReactive usage caution
Shallow data structures should only be used for root level state in a component. Avoid nesting them inside a deep reactive object as it creates a tree with inconsistent reactivity behavior that is difficult to understand and debug.
shallowReadonly() signature and behavior
shallowReadonly has the signature: function shallowReadonly<T extends object>(target: T): Readonly<T>. It is a shallow version of readonly() with no deep conversion - only root-level properties are made readonly. Property values are stored as-is, and properties with ref values will NOT be automatically unwrapped.
shallowReadonly example: nested properties mutable
Example: const state = shallowReadonly({ foo: 1, nested: { bar: 2 } }); state.foo++; // fails; isReadonly(state.nested) // false; state.nested.bar++; // works
toRaw() signature and purpose
toRaw has the signature: function toRaw<T>(proxy: T): T. It returns the raw, original object of a Vue-created proxy, working with proxies created by reactive(), readonly(), shallowReactive(), or shallowReadonly().
toRaw() use case and caution
toRaw is an escape hatch to temporarily read without incurring proxy access/tracking overhead or write without triggering changes. It is not recommended to hold a persistent reference to the original object and should be used with caution.
toRaw example
Example: const foo = {}; const reactiveFoo = reactive(foo); console.log(toRaw(reactiveFoo) === foo) // true
markRaw() signature and purpose
markRaw has the signature: function markRaw<T extends object>(value: T): T. It marks an object so it will never be converted to a proxy and returns the object itself.
markRaw example: preventing proxy conversion
Example: const foo = markRaw({}); console.log(isReactive(reactive(foo))) // false. Also works when nested: const bar = reactive({ foo }); console.log(isReactive(bar.foo)) // false
markRaw identity hazards pitfall
markRaw and shallow APIs allow opting out of deep conversion, but the raw opt-out is only at the root level. If you set a nested non-marked raw object into a reactive object and access it again, you get the proxied version back. This can lead to identity hazards where the same object exists as both raw and proxied versions. Example: const foo = markRaw({ nested: {} }); const bar = reactive({ nested: foo.nested }); console.log(foo.nested === bar.nested) // false
markRaw use cases
markRaw should be used when: some values should not be made reactive (complex 3rd party class instances, Vue component objects), or skipping proxy conversion provides performance improvements when rendering large lists with immutable data sources.
effectScope() signature and interface
effectScope has the signature: function effectScope(detached?: boolean): EffectScope where EffectScope interface is: { run<T>(fn: () => T): T | undefined; stop(): void }. It creates an effect scope object that captures reactive effects (computed and watchers) created within it so they can be disposed together.
effectScope example: capturing and disposing effects
Example: const scope = effectScope(); scope.run(() => { const doubled = computed(() => counter.value * 2); watch(doubled, () => console.log(doubled.value)); watchEffect(() => console.log('Count: ', doubled.value)); }); scope.stop(); // disposes all effects in the scope
getCurrentScope() signature and purpose
getCurrentScope has the signature: function getCurrentScope(): EffectScope | undefined. It returns the current active effect scope if there is one.
onScopeDispose() signature and purpose
onScopeDispose has the signature: function onScopeDispose(fn: () => void, failSilently?: boolean): void. It registers a dispose callback on the current active effect scope. The callback is invoked when the associated effect scope is stopped.
onScopeDispose() use as non-component-coupled replacement for onUnmounted
onScopeDispose can be used as a replacement for onUnmounted in reusable composition functions since each Vue component's setup() function is invoked in an effect scope.
onScopeDispose() warning behavior
A warning is thrown if onScopeDispose is called without an active effect scope. In Vue 3.5+, this warning can be suppressed by passing true as the second argument (failSilently parameter).