16 useRef - Referenzen und imperative Zugriffe

Der useRef Hook eröffnet uns eine völlig neue Dimension in React-Anwendungen, die sich grundlegend von allem unterscheidet, was wir bisher mit useState, useEffect und anderen Hooks kennengelernt haben. Während diese Hooks reaktiv funktionieren und Änderungen sofort im User Interface widerspiegeln, ermöglicht uns useRef einen direkten, imperativen Zugang zu DOM-Elementen und das Speichern von Werten, die zwischen Komponenten-Renders bestehen bleiben, ohne dabei selbst ein Re-Rendering auszulösen.

Diese einzigartige Eigenschaft macht useRef zu einem mächtigen Werkzeug für Szenarien, in denen die reaktive Natur von React an ihre Grenzen stößt oder nicht die optimale Lösung darstellt. Der Hook bedient zwei fundamental verschiedene Anwendungsbereiche, die auf den ersten Blick wenig gemeinsam haben, aber beide auf dem gleichen zugrunde liegenden Mechanismus basieren: dem Erstellen einer persistenten Referenz, die über Component-Lifecycles hinweg stabil bleibt.

16.1 Das Konzept der Referenzen verstehen

Eine Referenz, wie sie useRef erstellt, ist im Grunde ein Container-Objekt mit einer einzigen Eigenschaft namens current. Dieser Container selbst ändert sich niemals während der gesamten Lebensdauer einer Komponente. Was sich ändern kann, ist der Inhalt der current-Eigenschaft. Diese Stabilität ist der Schlüssel zu allen Anwendungsmöglichkeiten von useRef und unterscheidet Referenzen fundamental von State-Variablen.

Wenn wir eine Referenz mit useRef erstellen, erhalten wir ein Objekt, das React intern verwaltet und das bei jedem Render der Komponente identisch bleibt. Dies steht im starken Kontrast zu normalen Variablen innerhalb einer Funktionskomponente, die bei jedem Render neu erstellt werden, oder zu State-Variablen, die zwar zwischen Renders bestehen bleiben, aber deren Änderung einen neuen Render-Zyklus auslöst.

Die TypeScript-Integration von useRef ist besonders elegant gelöst. Der Hook ist generisch typisiert, wodurch wir den Typ des Wertes angeben können, den wir in der current-Eigenschaft speichern möchten. Für DOM-Referenzen verwenden wir die spezifischen HTML-Element-Typen wie HTMLInputElement oder HTMLDivElement, für Werte nutzen wir den entsprechenden Datentyp oder lassen TypeScript den Typ inferieren.

16.2 DOM-Zugriff - Der klassische Anwendungsfall

Der ursprüngliche und vermutlich bekannteste Einsatzbereich für useRef ist der direkte Zugriff auf DOM-Elemente. In traditionellem JavaScript war es normal, Elemente über document.getElementById oder ähnliche Methoden zu referenzieren und dann imperative Operationen wie Fokussierung, Scroll-Verhalten oder das Auslesen von Eigenschaften durchzuführen. React verfolgt normalerweise einen deklarativen Ansatz, bei dem wir den gewünschten Zustand beschreiben und React die DOM-Manipulation übernimmt.

Es gibt jedoch Situationen, in denen der imperative Zugang unvermeidlich oder deutlich praktischer ist. Typische Beispiele umfassen das programmatische Fokussieren von Input-Feldern, das Starten oder Stoppen von Videos, das Scrollen zu bestimmten Positionen, das Messen von Element-Dimensionen oder das Integrieren von Third-Party-Bibliotheken, die direkten DOM-Zugang benötigen.

Um eine DOM-Referenz zu erstellen, deklarieren wir zunächst eine Ref-Variable mit dem entsprechenden HTML-Element-Typ. Der Initialwert ist dabei immer null, da das DOM-Element zum Zeitpunkt der Ref-Erstellung noch nicht existiert. Anschließend verbinden wir die Referenz über das spezielle ref-Attribut mit dem gewünschten JSX-Element. React sorgt automatisch dafür, dass die current-Eigenschaft auf das tatsächliche DOM-Element gesetzt wird, sobald die Komponente gemountet wurde.

Bei der Verwendung von DOM-Referenzen ist es wichtig, immer mit der Möglichkeit zu rechnen, dass current den Wert null haben könnte. Dies kann während des initialen Renderings der Fall sein oder wenn die Komponente bereits unmounted wurde. Der Optional-Chaining-Operator von TypeScript (?.) ist hier ein nützlicher Helfer, um sichere DOM-Operationen durchzuführen.

Die Performance-Auswirkungen von DOM-Referenzen sind in der Regel vernachlässigbar, da das Erstellen und Verwalten von Referenzen sehr effizient ist. Wichtig ist jedoch zu verstehen, dass imperative DOM-Operationen außerhalb des React-Render-Zyklus stattfinden und daher möglicherweise nicht mit anderen State-Änderungen synchronisiert sind.

16.3 Persistente Werte - Der weniger bekannte Anwendungsfall

Der zweite große Anwendungsbereich für useRef ist das Speichern von Werten, die zwischen Komponenten-Renders bestehen bleiben sollen, ohne dabei ein Re-Rendering auszulösen. Dies ist besonders nützlich für Timer-IDs, Intervalreferenzen, das Verfolgen vorheriger Props oder State-Werte, Render-Zähler oder das Zwischenspeichern von berechneten Werten, die nicht im UI dargestellt werden sollen.

Der fundamentale Unterschied zu useState liegt in der Reaktivität. Während eine Änderung an einem State-Wert automatisch ein Re-Rendering der Komponente und aller Child-Komponenten auslöst, bleibt eine Änderung an einer Ref-Variable völlig unsichtbar für React. Der neue Wert ist sofort verfügbar, aber die Komponente “weiß” nichts von der Änderung und verhält sich so, als wäre nichts passiert.

Diese Eigenschaft macht Refs perfekt für Werte, die für die interne Logik einer Komponente wichtig sind, aber nicht direkt die Darstellung beeinflussen. Ein klassisches Beispiel ist die ID eines Timers oder Intervals, die wir benötigen, um den Timer später zu stoppen, die aber keine visuelle Repräsentation hat.

Ein weiterer häufiger Anwendungsfall ist das Verfolgen vorheriger Werte von Props oder State. Manchmal benötigen wir in einem useEffect oder einer anderen Funktion sowohl den aktuellen als auch den vorherigen Wert einer Variable, um zu bestimmen, was sich geändert hat. Da Funktionskomponenten bei jedem Render neu ausgeführt werden, gehen vorherige Werte normalerweise verloren. Eine Ref kann jedoch den vorherigen Wert speichern und ihn im nächsten Render verfügbar machen.

16.4 Der Unterschied zwischen useState und useRef

Die Unterscheidung zwischen useState und useRef ist fundamental für das Verständnis moderner React-Entwicklung. Beide Hooks dienen dem Speichern von Informationen zwischen Renders, aber sie verhalten sich völlig unterschiedlich in Bezug auf die Reaktivität der Komponente.

useState ist der reaktive Hook. Jede Änderung an einem State-Wert löst einen kompletten Re-Render-Zyklus aus, bei dem die gesamte Komponente neu ausgeführt wird, alle Hooks erneut berechnet werden und das UI aktualisiert wird, um den neuen State widerzuspiegeln. Dieser Prozess ist asynchron, das bedeutet, dass der neue State-Wert möglicherweise nicht sofort nach dem Aufruf der Setter-Funktion verfügbar ist.

useRef hingegen ist nicht-reaktiv. Das Ändern des Wertes einer Ref hat keinerlei Auswirkungen auf den Render-Zyklus der Komponente. Der neue Wert ist sofort und synchron verfügbar, aber React “bemerkt” die Änderung nicht und unternimmt nichts. Wenn wir möchten, dass eine Ref-Änderung im UI sichtbar wird, müssen wir zusätzlich einen State-Update oder ein manuelles Re-Rendering auslösen.

Diese Unterschiede haben wichtige Konsequenzen für die Performance. State-Updates können teuer sein, besonders in großen Komponentenbäumen oder bei häufigen Änderungen. Ref-Updates sind dagegen praktisch kostenlos. Wenn wir einen Wert speichern müssen, der sich häufig ändert, aber nicht zwingend sofort im UI reflektiert werden muss, ist eine Ref oft die bessere Wahl.

16.5 Troubleshooting

Einer der häufigsten Fehler beim Arbeiten mit useRef ist das Vergessen des current-Zugriffs. Da useRef ein Objekt mit einer current-Eigenschaft zurückgibt, müssen wir immer über diese Eigenschaft auf den tatsächlichen Wert zugreifen. Das direkte Verwenden der Ref-Variable führt zu Verwirrung und Fehlern.

Ein weiterer typischer Fehler ist das Erwarten von Reaktivität bei Ref-Änderungen. Entwickler, die von useState gewöhnt sind, dass Änderungen sofort im UI sichtbar werden, sind oft überrascht, wenn sich eine Ref-Änderung nicht auswirkt. Das Verständnis, dass Refs bewusst nicht-reaktiv sind, ist entscheidend für ihren korrekten Einsatz.

Bei DOM-Referenzen ist es wichtig, mit null-Werten umzugehen. Das DOM-Element ist nicht sofort verfügbar, und die Komponente kann unmounted werden, bevor eine geplante Operation ausgeführt wird. Der defensive Umgang mit Optional Chaining oder explizite null-Checks sind unverzichtbar.

Ein subtiler, aber wichtiger Punkt ist das Timing von Ref-Zuweisungen. DOM-Referenzen werden erst nach dem Mounting der Komponente verfügbar. Operationen, die sofort beim ersten Render ausgeführt werden sollen, müssen in einem useEffect mit leerer Dependency-Liste oder in einem Layout-Effect gekapselt werden.

16.6 Integration mit anderen Hooks

useRef harmoniert besonders gut mit useEffect, da beide Hooks den komponentenübergreifenden Zustand verwalten. Ein typisches Pattern ist das Speichern von Timer-IDs oder Event-Listener-Referenzen in einer Ref und das Cleanup in einem useEffect. Die Ref stellt sicher, dass die ID zwischen Renders verfügbar bleibt, während useEffect den ordnungsgemäßen Cleanup beim Unmounting übernimmt.

Bei der Verwendung mit useCallback oder useMemo ist Vorsicht geboten. Da Ref-Änderungen keine Dependency-Arrays triggern, können veraltete Werte in memoisierten Funktionen oder Werten “eingefroren” werden. Wenn eine memoizierte Funktion auf eine Ref zugreift, sollte diese Ref normalerweise nicht in das Dependency-Array aufgenommen werden, da sich das Ref-Objekt selbst nie ändert.

Die Kombination mit useContext kann mächtig sein, um Refs global verfügbar zu machen. Ein gemeinsamer useRef in einem Context ermöglicht es mehreren Komponenten, auf das gleiche DOM-Element oder den gleichen persistenten Wert zuzugreifen. Dies ist besonders nützlich für modale Dialoge, Focus-Management oder globale Timer.

16.7 Performance-Überlegungen

Die Performance-Charakteristika von useRef sind durchweg positiv. Das Erstellen einer Ref ist eine sehr kostengünstige Operation, und das Ändern von Ref-Werten hat praktisch keine Performance-Auswirkungen. Im Gegensatz zu State-Updates triggern Ref-Änderungen keine Render-Zyklen und verursachen daher keine Cascade-Updates in Child-Komponenten.

Für häufig ändernde Werte, die nicht sofort im UI reflektiert werden müssen, kann useRef eine deutliche Performance-Verbesserung gegenüber useState bieten. Beispiele sind Tracking-Daten, interne Zähler oder temporäre Berechnungen.

Bei DOM-Referenzen ist zu beachten, dass imperative DOM-Operationen außerhalb von Reacts Optimierungen stattfinden. Häufige DOM-Manipulationen über Refs können daher weniger effizient sein als deklarative Ansätze über State und Props.

16.8 Erweiterte Patterns

Ein fortgeschrittenes Pattern ist die Verwendung von Callback-Refs für dynamische Situationen. Anstatt einer statischen Ref können wir eine Funktion als ref-Attribut verwenden, die bei jeder Änderung des referenzierten Elements aufgerufen wird. Dies ist besonders nützlich bei Listen mit dynamischen Elementen oder when wir auf Mounting/Unmounting von Elementen reagieren müssen.

Für komplexere Szenarien können wir mehrere Refs in einem einzelnen useRef kombinieren, indem wir ein Objekt oder ein Array als Ref-Wert verwenden. Dies reduziert die Anzahl der Hook-Calls und kann die Übersichtlichkeit bei verwandten Referenzen verbessern.

Die TypeScript-Integration sollte immer vollständig ausgenutzt werden. Spezifische HTML-Element-Typen für DOM-Refs und präzise Typisierung für Wert-Refs helfen dabei, Laufzeit-Fehler zu vermeiden und die Entwicklungserfahrung zu verbessern.