customRef, track und trigger im Detail
Ein Custom Ref gibt vollständige Kontrolle über Dependency Tracking und Update-Auslösung. Wer die customRef API versteht, kann Debounce, Validierung, Storage-Synchronisation und andere Muster als sauberes, wiederverwendbares reaktives Primitiv kapseln, statt sie in jeder Komponente neu zusammenzuklicken.
Inhaltsverzeichnis
- 1. Warum ein Custom Ref überhaupt bauen
- 2. Die customRef API im Detail: track und trigger
- 3. Ein Custom Ref für Debounced Search Input
- 4. Custom Ref mit Validierung und Rollback
- 5. Custom Ref mit localStorage synchronisieren
- 6. Custom Refs typisieren mit TypeScript
- 7. Custom Refs mit Vitest testen
- 8. Häufige Fehler beim Bau eigener Refs
- 9. Custom Ref im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein Custom Ref überhaupt bauen
Ein Custom Ref ist ein reaktives Primitiv, das sich wie ein normales ref() verhält, dessen Dependency Tracking und Update-Auslösung aber vollständig selbst kontrolliert werden. Standardmäßig koppelt Vue bei ref() das Lesen automatisch an track() und jedes Schreiben automatisch an trigger(). Ein Custom Ref durchbricht diese feste Kopplung: Man entscheidet selbst, wann eine Abhängigkeit registriert wird und wann eine Komponente ein Update erhält.
Das klingt zunächst nach einem Nischenfall, ist in der Praxis aber der saubere Weg, um Muster wie Debounce, Validierung mit Rollback oder Synchronisation mit externen Datenquellen zu kapseln. Ohne Custom Ref landet diese Logik oft verstreut in watch()-Callbacks und lokalen Timern, direkt in der Komponente. Mit einem Custom Ref wird dieselbe Logik zu einer testbaren, wiederverwendbaren Funktion, die sich anfühlt wie ein gewöhnlicher reaktiver Wert.
Der Vorteil zeigt sich besonders in Composables. Eine Funktion, die einen Custom Ref zurückgibt, versteckt die interne Komplexität komplett vor dem Aufrufer. Die Komponente schreibt myValue.value = x wie bei jedem anderen Ref, merkt aber nichts davon, dass im Hintergrund ein Debounce-Timer läuft oder ein Netzwerk-Request abgesetzt wird. Diese Kapselung ist der eigentliche Wert eines eigenen Custom Ref.
2. Die customRef API im Detail: track und trigger
Die Funktion customRef() erwartet eine Factory-Funktion, die zwei Parameter erhält: track und trigger. Diese Factory muss ein Objekt mit get() und set(value) zurückgeben, ähnlich wie ein JavaScript-Property-Descriptor. In get() ruft man track() auf, um Vue mitzuteilen, dass hier eine reaktive Abhängigkeit gelesen wird. In set() ruft man nach der eigentlichen Wertänderung trigger() auf, um alle abhängigen Effekte neu auszuführen.
Der entscheidende Unterschied zu einem normalen ref(): Zwischen dem Setzen des internen Werts und dem Aufruf von trigger() kann beliebige Logik stehen, inklusive asynchroner Verzögerung. Genau das macht ein Custom Ref zum passenden Werkzeug für Debounce. Ein normales ref() löst trigger() synchron und sofort bei jeder Zuweisung aus, ein Custom Ref kann diesen Zeitpunkt frei bestimmen.
Wichtig ist, dass track() bei jedem get()-Aufruf ausgeführt werden muss, sonst bemerkt Vue nicht, dass eine Komponente von diesem Wert abhängt, und Updates bleiben aus. Wird trigger() vergessen, sieht die Komponente niemals eine Änderung, obwohl der interne Wert korrekt aktualisiert wurde. Beide Aufrufe sind also keine Optionen, sondern Pflichtbestandteil jedes funktionierenden Custom Ref.
import { customRef } from 'vue'
// Minimal custom ref: behaves exactly like ref(), fully manual
function myRef(initialValue) {
let value = initialValue
return customRef((track, trigger) => {
return {
get() {
track() // register this read as a dependency
return value
},
set(newValue) {
value = newValue
trigger() // notify all dependent effects
}
}
})
}
const count = myRef(0)
console.log(count.value) // 0
count.value = 5 // triggers re-render of anything reading count.value
3. Ein Custom Ref für Debounced Search Input
Das klassische Einsatzgebiet für einen Custom Ref ist ein Sucheingabefeld, das nicht bei jedem Tastendruck einen Request auslösen soll. Statt den Debounce in der Komponente mit watch() und setTimeout zu implementieren, kapselt man ihn direkt im Custom Ref. Die Komponente bindet das Ref ganz normal mit v-model, das Debounce-Verhalten bleibt intern und unsichtbar.
Der Trick liegt darin, dass set() den internen Wert sofort aktualisiert, damit das Eingabefeld selbst ohne Verzögerung reagiert, aber trigger() erst nach Ablauf des Timers aufgerufen wird. So bleibt das Textfeld responsiv, während alle abhängigen Effekte, etwa ein API-Aufruf in einem watch(), erst nach der Debounce-Zeit ausgelöst werden. Das ist ein Vorteil, den man mit reinem watch()-Debounce nur mit zusätzlichem lokalem State nachbilden könnte.
import { customRef } from 'vue'
// Custom ref that delays the trigger, not the internal value
export function useDebouncedRef(initialValue, delay = 300) {
let value = initialValue
let timeout
return customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue) {
value = newValue
clearTimeout(timeout)
timeout = setTimeout(() => {
trigger() // fires only after the user stops typing
}, delay)
}
}))
}
// In a component:
// const searchTerm = useDebouncedRef('')
// watch(searchTerm, (term) => fetchResults(term))
4. Custom Ref mit Validierung und Rollback
Ein weiteres starkes Einsatzfeld für einen Custom Ref ist Validierung mit automatischem Rollback bei ungültigen Werten. Statt in jeder Komponente manuell zu prüfen, ob ein Eingabewert erlaubt ist, kapselt man diese Regel im set() des Custom Ref. Ist der neue Wert ungültig, wird er einfach verworfen und trigger() nicht aufgerufen, der alte Wert bleibt sichtbar bestehen.
Diese Technik eignet sich besonders für numerische Eingaben mit Grenzwerten, etwa Mengenfelder in einem Warenkorb, die niemals negativ werden dürfen. Der Custom Ref kann zusätzlich einen Fehlerzustand als separaten reaktiven Wert exponieren, sodass die Komponente eine Fehlermeldung anzeigen kann, ohne die Validierungslogik selbst zu kennen. Das hält Komponenten schlank und die Validierungsregel an einer einzigen Stelle wiederverwendbar.
import { customRef, ref } from 'vue'
// Custom ref with validation and automatic rollback on invalid input
export function useValidatedNumber(initialValue, { min = 0, max = Infinity } = {}) {
let value = initialValue
const error = ref(null)
const numberRef = customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue) {
const parsed = Number(newValue)
if (Number.isNaN(parsed) || parsed < min || parsed > max) {
error.value = `Wert muss zwischen ${min} und ${max} liegen`
return // rollback: internal value stays unchanged, no trigger
}
error.value = null
value = parsed
trigger()
}
}))
return { value: numberRef, error }
}
5. Custom Ref mit localStorage synchronisieren
Ein Custom Ref eignet sich hervorragend, um reaktiven State transparent mit localStorage zu synchronisieren. Beim Lesen liefert get() den aktuellen In-Memory-Wert, beim Schreiben aktualisiert set() sowohl den internen Wert als auch den Eintrag im Browser-Storage. Aus Sicht der Komponente sieht das Ref aus wie jeder andere reaktive Wert, die Persistenz passiert vollständig transparent im Hintergrund.
Interessant wird es, wenn man zusätzlich auf das storage-Event des Browsers hört, das bei Änderungen in einem anderen Tab feuert. Der Custom Ref kann dann bei einem externen Storage-Event trigger() aufrufen, ohne dass set() lokal aufgerufen wurde, und so Cross-Tab-Synchronisation komplett innerhalb des Refs kapseln. Das ist ein Muster, das mit einem normalen ref() nicht sauber abbildbar wäre, weil dort kein Kontrollpunkt für externe Trigger existiert.
import { customRef } from 'vue'
// Custom ref synced with localStorage, including cross-tab updates
export function useStorageRef(key, initialValue) {
let value = localStorage.getItem(key)
? JSON.parse(localStorage.getItem(key))
: initialValue
let triggerFn
window.addEventListener('storage', (event) => {
if (event.key === key && triggerFn) {
value = JSON.parse(event.newValue)
triggerFn() // sync value changed in another browser tab
}
})
return customRef((track, trigger) => {
triggerFn = trigger
return {
get() {
track()
return value
},
set(newValue) {
value = newValue
localStorage.setItem(key, JSON.stringify(newValue))
trigger()
}
}
})
}
6. Custom Refs typisieren mit TypeScript
In TypeScript profitiert ein Custom Ref stark von generischen Typparametern. customRef<T>() erlaubt es, den zurückgegebenen Wert exakt zu typisieren, sodass ein Aufrufer korrekte Autovervollständigung erhält, genau wie bei einem normalen Ref<T>. Der Rückgabetyp der Factory-Funktion muss dem internen CustomRefFactory<T>-Interface entsprechen, das get(): T und set(value: T): void vorschreibt.
Ein häufiger Fehler ist, den generischen Typparameter wegzulassen und Vue den Typ aus dem Initialwert inferieren zu lassen. Das funktioniert bei einfachen Primitiven, scheitert aber bei komplexeren Union-Typen oder wenn der Custom Ref zunächst null enthält und später einen konkreten Wert annimmt. Explizite Typisierung mit customRef<string | null>() vermeidet solche Überraschungen und macht die API-Signatur des Composables selbstdokumentierend.
import { customRef, type Ref } from 'vue'
interface DebouncedRefOptions {
delay?: number
}
// Explicit generic typing for a reusable custom ref factory
export function useDebouncedRef<T>(
initialValue: T,
options: DebouncedRefOptions = {}
): Ref<T> {
const { delay = 300 } = options
let value = initialValue
let timeout: ReturnType<typeof setTimeout>
return customRef<T>((track, trigger) => ({
get(): T {
track()
return value
},
set(newValue: T) {
value = newValue
clearTimeout(timeout)
timeout = setTimeout(() => trigger(), delay)
}
}))
}
7. Custom Refs mit Vitest testen
Ein Custom Ref lässt sich isoliert testen, ohne eine Vue-Komponente zu mounten, da customRef() auch außerhalb des Component-Setups gültig einsetzbar ist. Für Tests mit zeitbasierter Logik wie Debounce ist vi.useFakeTimers() unverzichtbar: Ohne gefälschte Timer müsste ein Test real 300 Millisekunden warten, mit ihnen kann man die Zeit deterministisch vorspulen.
Beim Testen eines Custom Ref prüft man typischerweise drei Dinge: dass .value synchron den erwarteten internen Zustand liefert, dass reaktive Effekte, die das Ref beobachten, korrekt und zum richtigen Zeitpunkt auslösen, und dass ungültige Eingaben bei Validierungs-Refs tatsächlich zu einem Rollback führen. Ein watchEffect() im Test, kombiniert mit einem Spy, macht sichtbar, wie oft und wann trigger() tatsächlich gefeuert hat.
import { describe, it, expect, vi } from 'vitest'
import { watchEffect } from 'vue'
import { useDebouncedRef } from './useDebouncedRef'
describe('useDebouncedRef', () => {
it('delays trigger until after the debounce window', () => {
vi.useFakeTimers()
const debounced = useDebouncedRef('', 300)
const spy = vi.fn()
watchEffect(() => {
spy(debounced.value)
})
debounced.value = 'a'
debounced.value = 'ab'
debounced.value = 'abc'
expect(spy).toHaveBeenCalledTimes(1) // only the initial run so far
vi.advanceTimersByTime(300)
expect(spy).toHaveBeenCalledTimes(2) // trigger fired once, latest value
expect(debounced.value).toBe('abc')
vi.useRealTimers()
})
})
8. Häufige Fehler beim Bau eigener Refs
Der häufigste Fehler bei einem selbst gebauten Custom Ref ist, track() zu vergessen oder es nur bedingt aufzurufen. Wird track() etwa nur innerhalb eines if-Blocks ausgeführt, registriert Vue die Abhängigkeit nur manchmal, und Updates verschwinden scheinbar zufällig. track() muss bei jedem get()-Aufruf bedingungslos laufen, ganz gleich, welcher Wert zurückgegeben wird.
Ein zweiter typischer Fehler ist, den internen Wert eines Custom Ref außerhalb der Factory-Funktion in einem gemeinsam genutzten Modul-Scope zu speichern. Wird derselbe Composable mehrfach in verschiedenen Komponenten aufgerufen, teilen sich dann versehentlich alle Instanzen denselben Zustand, weil die Closure nicht pro Aufruf neu erzeugt wird. Jeder Aufruf der Factory-Funktion muss eine eigene, unabhängige Closure-Variable für den internen Wert anlegen.
9. Custom Ref im Vergleich zu Alternativen
Nicht jedes reaktive Muster erfordert einen eigenen Custom Ref. Die folgende Übersicht zeigt, wann ein Custom Ref die richtige Wahl ist und wann einfachere Bordmittel ausreichen.
| Anforderung | Ohne Custom Ref | Mit Custom Ref | Vorteil |
|---|---|---|---|
| Debounce bei v-model | watch + setTimeout in Komponente | useDebouncedRef() | Wiederverwendbar, Komponente bleibt schlank |
| Feldvalidierung mit Rollback | Manuelle Prüfung vor jeder Zuweisung | Validierung in set() | Ungültige Werte nie sichtbar |
| Cross-Tab-Sync | Eigene Event-Listener pro Komponente | Custom Ref mit storage-Event | Sync-Logik an einer Stelle gekapselt |
| Einfacher lokaler Wert | ref() | Unnötiger Overhead | customRef nur bei echtem Kontrollbedarf |
| Read-only Ableitung | Custom Ref mit leerem set() | computed() | Passenderes, einfacheres Primitiv |
Die Faustregel ist einfach: Ein Custom Ref lohnt sich immer dann, wenn zwischen dem Setzen eines Werts und dem Auslösen des Updates zusätzliche Logik nötig ist, sei es Zeitverzögerung, Validierung oder externe Synchronisation. Für alle Fälle, in denen Lesen und Schreiben direkt gekoppelt bleiben können, ist ein normales ref() die einfachere und genauso performante Wahl.
Mironsoft
Vue 3, Nuxt und Composition API Beratung
Reaktivität in Vue 3 wird bei euch zum Bugmagneten?
Wir analysieren eure Composables, bauen eigene Custom Refs für Debounce, Validierung und Storage-Sync und bringen eure Vue-3-Reaktivität auf ein wartbares Fundament.
Composable-Audit
Bestehende Refs und watch-Logik auf saubere Kapselung prüfen
Custom-Ref-Bibliothek
Debounce, Validierung und Storage-Sync als getestete Composables
TypeScript-Typisierung
Generische Refs mit korrekter Typinferenz für euer Team
10. Zusammenfassung
Ein Custom Ref ist das richtige Werkzeug, wenn zwischen dem Setzen eines Werts und dem Auslösen reaktiver Updates zusätzliche Logik nötig ist. Die customRef()-API mit ihren Parametern track und trigger gibt vollständige Kontrolle über Dependency Tracking und Update-Zeitpunkt. Debounce, Validierung mit Rollback und Storage-Synchronisation sind die drei häufigsten praktischen Anwendungsfälle, in denen ein eigener Custom Ref Code deutlich sauberer macht als verstreute watch()-Logik in der Komponente.
In TypeScript profitieren Custom Refs von expliziten generischen Typparametern, und beim Testen lassen sie sich isoliert mit gefälschten Timern prüfen, ohne eine komplette Komponente zu mounten. Wer track() und trigger() konsequent korrekt aufruft und den internen Zustand pro Factory-Aufruf sauber kapselt, erhält reaktive Primitive, die sich für den Aufrufer nicht anders anfühlen als ein gewöhnliches ref(), intern aber deutlich mehr können.
Custom Refs in Vue 3 — Das Wichtigste auf einen Blick
customRef API
Factory mit track und trigger als Parametern, Rückgabe von get()/set() wie ein Property-Descriptor.
Typische Anwendungsfälle
Debounce, Validierung mit Rollback, localStorage-Sync inklusive Cross-Tab-Updates.
Häufigster Fehler
track() bedingt oder gar nicht aufrufen — Updates bleiben dann scheinbar zufällig aus.
Testen
Isoliert außerhalb von Komponenten möglich, mit vi.useFakeTimers() für zeitbasierte Refs.