How would you build a debounced ref with Vue 3's `customRef`, and what do the `track` and `trigger` arguments of its factory do?
answer
- a factory returning get and set
- track in get, trigger in set
- trigger delayed by a timer
- you decide when readers update
basics
~20 scustomRef takes a factory that receives track and trigger and returns get and set. Call track() in get so readers subscribe; in set, restart a timer and only when it fires store the value and call trigger(), so readers update once typing pauses.
solid answer
~40 s`customRef(factory)` hands the factory two functions bound to the ref's own dep: `track()` subscribes the currently running effect, and `trigger()` notifies every subscriber. The factory returns `{ get, set }`, and Vue calls them for `.value` reads and writes - with no automatic tracking or triggering. For a debounced ref, `get` calls `track()` and returns the stored value; `set` clears a pending `setTimeout` and schedules a new one that stores the value and calls `trigger()`. Bound with `v-model`, each keystroke calls `set`, but readers re-render once, after the delay. Keep the getter returning a stable value: the docs warn that a getter producing a new object on every read can make a child that receives it as a prop update without `set` ever running.
code
ts · 18 linesimport { customRef } from 'vue'
export function useDebouncedRef<T>(value: T, delay = 300) {
let timeout: ReturnType<typeof setTimeout> | undefined
return customRef<T>((track, trigger) => ({
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
},
}))
}go deeper
Recall that customRef exists for refs with custom read and write behaviour, with debouncing as the textbook example.
Explain that track and trigger are the ref's own dep controls, and write the debounced ref from memory with its timer in set.
Judge when customRef beats a ref plus watch, and guard against unstable getters and timers that outlive their component.
Decide whether custom reactive primitives belong in a shared composable library, and document their notification rules for the team.
## What `customRef` gives you A normal `ref` hard-wires the reactivity rules: its `.value` getter always calls `track()`, and its setter calls `trigger()` whenever the value changes. `customRef` hands those two decisions to you. Its signature, from the API reference: ```ts function customRef<T>(factory: CustomRefFactory<T>): Ref<T> type CustomRefFactory<T> = ( track: () => void, trigger: () => void ) => { get: () => T set: (value: T) => void } ``` In the source, the ref creates its own `Dep` and passes the factory `dep.track` and `dep.trigger`, bound to that dep. Reading `.value` just calls your `get`; writing `.value` just calls your `set`. Nothing else happens automatically. ## The two arguments | Argument | What it does | Where you usually call it | |---|---|---| | `track()` | links this ref's dep to the currently running effect (render, computed, watcher); does nothing if none is running | inside `get` | | `trigger()` | bumps the dep's version and notifies every subscribed effect | inside `set`, when readers should update | Skipping `track()` makes the ref non-reactive for readers; calling `trigger()` at a moment you choose is the whole point of the API. ## Building the debounced ref 1. Keep the real value and a timer handle in the factory's closure. 2. In `get`, call `track()` and return the value. 3. In `set`, clear any pending timer and start a new one. 4. When the timer fires, store the new value and call `trigger()`. With `<input v-model="query">`, each keystroke calls `set`. The input element shows what the user typed, because the DOM owns that text; everything that *reads* `query` - a computed filter, a watcher that fetches - sees one change after typing pauses. ## Cleaning up A pending timer can still fire after the component that owns the ref is gone. In a composable, clear it when the owning scope is disposed so no late `trigger()` runs against dead state. ## Pitfalls - **Unstable getter results.** If `get` builds a new object or array on every call and the ref is passed as a prop, each parent re-render produces a new prop value; the child sees a changed prop and updates, although `set` never ran. Return the same stored value from `get`. - **Forgetting `track()`.** The ref still returns values, but no effect subscribes, so `trigger()` notifies nobody. - **A re-render during the delay.** If some other state re-renders the component while a timer is pending, `v-model` re-applies the ref's current (old) value to the input and the user's latest keystrokes disappear. Keep unrelated reactive state out of that component, or debounce a second ref instead. - **Calling `trigger()` without changing anything.** Unlike a normal ref, a `customRef` has no built-in equality check; every `trigger()` notifies subscribers. ## When to reach for it - Debounced or throttled input bound with `v-model`. - A ref whose writes are validated, clamped or transformed before readers see them. - Bridging an external value source where you decide when a change counts. For most cases a plain `ref` plus a debounced `watch` writing a second ref works too; `customRef` wins when you want one ref that callers use exactly like any other. ## customRef compared with a plain ref | Behaviour | `ref()` | `customRef()` | |---|---|---| | Tracking on read | automatic in the `.value` getter | only if your `get` calls `track()` | | Notification on write | automatic, only when `Object.is` says the value changed | only when your code calls `trigger()` | | Deep conversion of objects | yes, object values become reactive | no, you return whatever you stored | | Timing of notification | synchronous with the write | whenever you choose, including later | The table is the real answer to the interview question: a `customRef` is a ref with the automatic parts removed, so you rebuild exactly the parts you need.
- What does the Vue documentation warn about returning a new object from a `customRef` getter?If that ref is passed as a prop, every parent re-render calls the getter and hands the child a new object. The child compares it with its previous prop, sees a difference and updates, even though the setter and `trigger()` never ran. Return a stable stored value instead.
- Could you get the same debounce without `customRef`?Yes: keep a source `ref` bound to the input and a `watch` on it that writes a second ref after a timeout. It uses only everyday APIs but splits one concept into two refs; `customRef` keeps a single ref that behaves like any other with `v-model`.
saying these in an interview costs you the question
- customRef tracks and triggers automatically; track and trigger are optional extras.
- trigger() must be called synchronously inside set or Vue throws.
- A pending debounce timer cannot fire once its component has unmounted.
- customRef skips notifying when the new value equals the old one.
- The getter can safely build a fresh object on every read.