React Hooks: Der useRef Hook

Basics-Trilogie

23.05.2024

reactde
Gregor Wedlich
Gregor Wedlich
Life, the Universe and Everything.
Donate with: Alby

Inhaltsverzeichnis

    In dieser Reihe schauen wir uns die verschiedenen React Hooks an und wie wir sie einsetzen können. Ich verfolge dabei keinen tieferen Plan oder ein Skript, daher könnte es gut möglich sein, dass du dich jetzt fragst, wieso fängt der denn jetzt mit dem useRef-Hook an? Ehrlich gesagt, weiß ich das auch nicht. Seht es mir also nicht nach, ich schreibe hier einfach nur Dinge nieder, und vielleicht hilft es ja doch jemandem da draußen.

    Die Basics-Trilogie

    Bevor wir loslegen: Dieser Artikel ist Teil meiner "Basics-Trilogie", in der ich die fundamentalen React Hooks erkläre - nur halt in einer völlig verkehrten Reihenfolge, weil... warum nicht?

    Die komplette Reihe:

    1. Der useRef Hook ← Du bist hier!
    2. Der useEffect Hook - Der Hook für Seiteneffekte
    3. Der useState Hook - Die "Basics" zum Schluss

    Falls du hier gelandet bist und denkst "Häh, warum fängt der nicht mit useState an?" - keine Sorge, am Ende macht alles Sinn. Versprochen!

    Lange Rede, kurzer Sinn, lasst uns beginnen.

    Was ist useRef überhaupt?

    Okay, fangen wir mal ganz vorne an. useRef ist wie dein persönlicher Notizzettel in React. Stell dir vor, du hast einen Post-it, der an deinem Monitor klebt. Egal wie oft du deinen Bildschirm aus- und wieder einschaltest (oder React deine Komponente neu rendert), der Zettel bleibt da kleben.

    Der große Unterschied zu useState? Wenn du etwas auf deinen Notizzettel kritzelst, schreit React nicht "ALLES NEU ZEICHNEN!" - es bemerkt die Änderung einfach nicht. Und genau das wollen wir manchmal!

    Der Unterschied zu useState

    Bevor wir richtig loslegen, lass uns den Unterschied zwischen useState und useRef einmal live erleben:

    1import { useState, useRef } from "react"; 2 3function StateVsRef() { 4 // This counter triggers a re-render on every change 5 const [stateCounter, setStateCounter] = useState<number>(0); 6 7 // This counter works in the background - no re-renders 8 const refCounter = useRef<number>(0); 9 10 // Bonus: Counts how many times this component has rendered 11 const renderCounter = useRef<number>(0); 12 renderCounter.current++; 13 14 return ( 15 <div style={{ padding: "20px", fontFamily: "monospace" }}> 16 <h2>State vs Ref - The Showdown</h2> 17 18 <div 19 style={{ marginBottom: "20px", padding: "10px", background: "#6c6262" }} 20 > 21 <p>🔄 Component has rendered {renderCounter.current}x</p> 22 </div> 23 24 <div style={{ display: "flex", gap: "40px" }}> 25 <div> 26 <h3>useState Counter: {stateCounter}</h3> 27 <button 28 onClick={() => { 29 setStateCounter(stateCounter + 1); 30 console.log("State changed - React re-renders!"); 31 }} 32 > 33 Increase State 34 </button> 35 </div> 36 37 <div> 38 <h3>useRef Counter: {refCounter.current}</h3> 39 <button 40 onClick={() => { 41 refCounter.current++; 42 console.log("Ref is now:", refCounter.current); 43 alert( 44 `Ref counter is at ${ 45 refCounter.current 46 }, but the display stays at ${refCounter.current - 1}!` 47 ); 48 }} 49 > 50 Increase Ref 51 </button> 52 </div> 53 </div> 54 </div> 55 ); 56} 57 58export default StateVsRef;

    Probier das mal aus! Du wirst sehen: Der State-Zähler aktualisiert brav die Anzeige, während der Ref-Zähler sich zwar erhöht (schau in die Konsole!), aber die Anzeige stur bei 0 bleibt. Das ist kein Bug, das ist ein Feature!

    Beispiele

    Der Klassiker: DOM-Elemente referenzieren

    Jetzt aber zum häufigsten Anwendungsfall: DOM-Elemente referenzieren. Hier ein praktisches Beispiel:

    1import { useRef, useState } from "react"; 2 3function FocusManager() { 4 const inputField = useRef<HTMLInputElement>(null); 5 const [text, setText] = useState<string>(""); 6 7 const focusInput = () => { 8 // Directly access the DOM element 9 inputField.current?.focus(); 10 }; 11 12 const selectAll = () => { 13 // More DOM magic 14 inputField.current?.select(); 15 }; 16 17 const insertText = () => { 18 // Set value directly (bypasses React!) 19 if (inputField.current) { 20 inputField.current.value = "Surprise! 🎉"; 21 // Important: Manually update state, otherwise React gets confused 22 setText("Surprise! 🎉"); 23 } 24 }; 25 26 return ( 27 <div style={{ padding: "20px" }}> 28 <h2>Input Field Magic with useRef</h2> 29 30 <input 31 ref={inputField} 32 type="text" 33 value={text} 34 onChange={(e) => setText(e.target.value)} 35 placeholder="Type something..." 36 style={{ 37 padding: "8px", 38 fontSize: "16px", 39 width: "300px", 40 marginBottom: "10px", 41 }} 42 /> 43 44 <div style={{ display: "flex", gap: "10px" }}> 45 <button onClick={focusInput}>Focus!</button> 46 <button onClick={selectAll}>Select All</button> 47 <button onClick={insertText}>Surprise</button> 48 </div> 49 </div> 50 ); 51} 52 53export default FocusManager;

    Timer und Intervalle

    Hier wird's richtig spannend. Kennst du das Problem? Du startest einen Timer in React, und dann... ja, wie stoppst du den wieder? Mit useRef wird's was:

    1import { useState, useRef, useEffect } from "react"; 2 3function PomodoroTimer() { 4 const [minutes, setMinutes] = useState<number>(25); 5 const [seconds, setSeconds] = useState<number>(0); 6 const [isActive, setIsActive] = useState<boolean>(false); 7 const [isPaused, setIsPaused] = useState<boolean>(false); 8 9 // The trick: Timer ID persists across all re-renders 10 const intervalRef = useRef<number | null>(null); 11 12 useEffect(() => { 13 if (isActive && !isPaused) { 14 intervalRef.current = setInterval(() => { 15 // Calculate total time in seconds 16 const totalTime = minutes * 60 + seconds; 17 18 if (totalTime <= 0) { 19 // Timer finished 20 setIsActive(false); 21 setIsPaused(false); 22 clearInterval(intervalRef.current!); 23 intervalRef.current = null; 24 alert("Pomodoro finished! Time for a break!"); 25 return; 26 } 27 28 // Calculate new time 29 const newTime = totalTime - 1; 30 const newMinutes = Math.floor(newTime / 60); 31 const newSeconds = newTime % 60; 32 33 // Update states 34 setMinutes(newMinutes); 35 setSeconds(newSeconds); 36 }, 1000); 37 } else { 38 // Stop timer 39 if (intervalRef.current) { 40 clearInterval(intervalRef.current); 41 intervalRef.current = null; 42 } 43 } 44 45 // Cleanup on unmount 46 return () => { 47 if (intervalRef.current) { 48 clearInterval(intervalRef.current); 49 intervalRef.current = null; 50 } 51 }; 52 }, [isActive, isPaused, minutes, seconds]); 53 54 const toggleTimer = () => { 55 setIsActive(!isActive); 56 setIsPaused(false); 57 }; 58 59 const pauseTimer = () => { 60 setIsPaused(!isPaused); 61 }; 62 63 const resetTimer = () => { 64 setMinutes(25); 65 setSeconds(0); 66 setIsActive(false); 67 setIsPaused(false); 68 }; 69 70 return ( 71 <div 72 style={{ 73 padding: "40px", 74 textAlign: "center", 75 background: isActive ? "#ffebee" : "#e8f5e9", 76 borderRadius: "10px", 77 transition: "background 0.3s", 78 }} 79 > 80 <h1 style={{ fontSize: "48px", margin: "0" }}> 81 {String(minutes).padStart(2, "0")}:{String(seconds).padStart(2, "0")} 82 </h1> 83 84 <div 85 style={{ 86 marginTop: "20px", 87 display: "flex", 88 gap: "10px", 89 justifyContent: "center", 90 }} 91 > 92 <button onClick={toggleTimer} style={{ padding: "10px 20px" }}> 93 {isActive ? "⏹️ Stop" : "▶️ Start"} 94 </button> 95 96 {isActive && ( 97 <button onClick={pauseTimer} style={{ padding: "10px 20px" }}> 98 {isPaused ? "▶️ Resume" : "⏸️ Pause"} 99 </button> 100 )} 101 102 <button onClick={resetTimer} style={{ padding: "10px 20px" }}> 103 🔄 Reset 104 </button> 105 </div> 106 </div> 107 ); 108} 109 110export default PomodoroTimer;

    Der Zeitreisende

    Manchmal willst du wissen, was der vorherige Wert war. Mit einem Custom Hook und useRef geht das super:

    1import { useRef, useEffect, useState } from "react"; 2 3// Our own hook! 4function usePrevious<T>(value: T): T | undefined { 5 const ref = useRef<T | undefined>(undefined); 6 7 useEffect(() => { 8 ref.current = value; 9 }); 10 11 return ref.current; 12} 13 14// Practical example: Crypto Ticker 15function KryptoTicker() { 16 const [bitcoinPrice, setBitcoinPrice] = useState<number>(45000); 17 const previous = usePrevious<number>(bitcoinPrice); 18 19 const change: number = previous ? bitcoinPrice - previous : 0; 20 const percent: string = previous 21 ? ((change / previous) * 100).toFixed(2) 22 : "0"; 23 24 const simulatePriceChange = (): void => { 25 // Random price change between -5% and +5% 26 const factor: number = 0.95 + Math.random() * 0.1; 27 setBitcoinPrice((prev) => Math.round(prev * factor)); 28 }; 29 30 return ( 31 <div 32 style={{ 33 padding: "20px", 34 background: "#1a1a1a", 35 color: "white", 36 borderRadius: "10px", 37 fontFamily: "monospace", 38 }} 39 > 40 <h2>₿ Bitcoin Ticker</h2> 41 42 <div style={{ fontSize: "32px", marginBottom: "10px" }}> 43 ${bitcoinPrice.toLocaleString()} 44 </div> 45 46 {previous && ( 47 <div 48 style={{ 49 color: change >= 0 ? "#4caf50" : "#f44336", 50 fontSize: "18px", 51 }} 52 > 53 {change >= 0 ? "📈" : "📉"} {change >= 0 ? "+" : ""} 54 {change.toLocaleString()}({percent}%) 55 </div> 56 )} 57 58 <button 59 onClick={simulatePriceChange} 60 style={{ 61 marginTop: "20px", 62 padding: "10px 20px", 63 background: "#2196f3", 64 color: "white", 65 border: "none", 66 borderRadius: "5px", 67 cursor: "pointer", 68 }} 69 > 70 Update Price 71 </button> 72 </div> 73 ); 74} 75 76export default KryptoTicker

    Der usePrevious Hook ist ein elegantes Beispiel für einen benutzerdefinierten React-Hook. Er speichert den vorherigen Wert einer Variable über Renderings hinweg.

    Schritt für Schritt:

    Speicher mit useRef:

    • Wir erstellen eine Ref mit useRef<T | undefined>(undefined)
    • useRef behält seinen Wert zwischen Renderings bei (im Gegensatz zu normalen Variablen)

    Timing-Trick mit useEffect:

    • Der useEffect läuft nach jedem Rendering (da keine Abhängigkeiten angegeben sind)
    • Er aktualisiert ref.current mit dem aktuellen Wert von value

    Der entscheidende Punkt:

    • Beim ersten Rendering ist ref.current noch undefined
    • Bei jedem weiteren Rendering gibt der Hook den Wert zurück, der während des vorherigen Renderings gespeichert wurde
    • Erst nach dem aktuellen Rendering wird der aktuelle Wert gespeichert

    One-Pager Navigation - Smooth Scrolling

    Ein typischer Anwendungsfall:

    1import { useRef } from "react"; 2 3function OnePager() { 4 const homeRef = useRef<HTMLElement>(null); 5 const aboutRef = useRef<HTMLElement>(null); 6 const projectRef = useRef<HTMLElement>(null); 7 const contactRef = useRef<HTMLElement>(null); 8 9 const scrollTo = (elementRef: React.RefObject<HTMLElement | null>) => { 10 elementRef.current?.scrollIntoView({ 11 behavior: "smooth", 12 block: "start", 13 }); 14 }; 15 16 const navStyle: React.CSSProperties = { 17 position: "sticky", 18 top: 0, 19 background: "rgba(255, 255, 255, 0.95)", 20 backdropFilter: "blur(10px)", 21 padding: "15px", 22 boxShadow: "0 2px 10px rgba(0,0,0,0.1)", 23 zIndex: 100, 24 display: "flex", 25 gap: "20px", 26 justifyContent: "center", 27 }; 28 29 const sectionStyle: React.CSSProperties = { 30 minHeight: "100vh", 31 padding: "80px 20px", 32 display: "flex", 33 flexDirection: "column", 34 justifyContent: "center", 35 alignItems: "center", 36 }; 37 38 return ( 39 <div> 40 <nav style={navStyle}> 41 <button onClick={() => scrollTo(homeRef)}>Home</button> 42 <button onClick={() => scrollTo(aboutRef)}>About Me</button> 43 <button onClick={() => scrollTo(projectRef)}>Projects</button> 44 <button onClick={() => scrollTo(contactRef)}>Contact</button> 45 </nav> 46 47 <section ref={homeRef} style={{ ...sectionStyle, background: "#e3f2fd" }}> 48 <h1>Welcome!</h1> 49 <p>Scroll or use the navigation</p> 50 </section> 51 52 <section 53 ref={aboutRef} 54 style={{ ...sectionStyle, background: "#f3e5f5" }} 55 > 56 <h1>About Me</h1> 57 <p>I am a React developer with a passion for hooks</p> 58 </section> 59 60 <section 61 ref={projectRef} 62 style={{ ...sectionStyle, background: "#e8f5e9" }} 63 > 64 <h1>My Projects</h1> 65 <p>Your ad could be here</p> 66 </section> 67 68 <section 69 ref={contactRef} 70 style={{ ...sectionStyle, background: "#fff3e0" }} 71 > 72 <h1>Contact</h1> 73 <p>Email me: max@mustermann.de</p> 74 </section> 75 </div> 76 ); 77} 78 79export default OnePager;

    Die goldene Regel: Wann useState, wann useRef?

    Nach all den Beispielen fragst du dich vielleicht: "Okay, aber wann nutze ich jetzt was?" Hier meine bewährte Faustregel:

    Stell dir eine einzige Frage: "Soll sich die Anzeige ändern, wenn sich dieser Wert ändert?"

    • JA → useState (die UI muss reagieren)
    • NEIN → useRef (im Hintergrund werkeln)

    Konkrete Beispiele:

    useState ist richtig für:

    • Formulareingaben, die angezeigt werden
    • Sichtbare Zähler, Punkte, Scores
    • Toggle-States (Dark Mode an/aus, Menü offen/zu)
    • Loading-States
    • Alles, was der User sehen soll

    useRef ist richtig für:

    • Timer- und Interval-IDs
    • DOM-Element-Referenzen
    • Vorherige Props/State-Werte zum Vergleichen
    • Canvas-Kontexte
    • Third-Party-Library-Instanzen
    • WebSocket-Verbindungen
    • Alles, was "hinter den Kulissen" läuft

    Häufige Fehler und wie du sie vermeidest

    Ein Fehler, den ich am Anfang ständig gemacht habe:

    1// ❌ WRONG - modifying ref.current during rendering 2function BadIdea() { 3 const counter = useRef(0); 4 counter.current++; // This can lead to chaos! 5 6 return <div>Render #{counter.current}</div>; 7} 8 9// ✅ CORRECT - modify ref.current in effects or event handlers 10function GoodIdea() { 11 const renderCounter = useRef(0); 12 13 useEffect(() => { 14 renderCounter.current++; 15 console.log(`Render #${renderCounter.current}`); 16 }); 17 18 const handleClick = () => { 19 renderCounter.current = 0; 20 console.log('Counter reset!'); 21 }; 22 23 return ( 24 <div> 25 <p>Check the console for the render counter</p> 26 <button onClick={handleClick}>Reset</button> 27 </div> 28 ); 29}

    Zum Abschluss

    useRef ist wie ein Schweizer Taschenmesser in React - unscheinbar, aber verdammt nützlich. Es löst Probleme, von denen du vielleicht noch gar nicht wusstest, dass du sie hast.

    Fang mit DOM-Referenzen an, spiel mit Timern rum, und bevor du dich versiehst, wirst du useRef für Sachen nutzen, die mir noch gar nicht eingefallen sind.

    Und hey, falls du dich immer noch fragst, warum ich mit useRef angefangen habe - vielleicht war's doch kein Zufall. Es ist einer dieser Hooks, der dir zeigt, dass React mehr ist als nur State hin- und herschieben. Es öffnet die Tür zu fortgeschrittenen Patterns und eleganteren Lösungen.

    Die Basics-Trilogie geht weiter!

    Das war Teil 1 der Basics-Trilogie. Bereit für mehr?

    Weiter geht's mit Der useEffect Hook

    Dort erkläre ich endlich, was dieser mysteriöse return im Timer-Beispiel macht. Spoiler: Es hat mit Aufräumen zu tun!

    Alle Beispiele zum Ausprobieren

    1. StateVsRef - Der Unterschied
    2. FokusManager - DOM-Elemente kontrollieren
    3. PomodoroTimer - Timer mit useRef verwalten
    4. KryptoTicker - Vorherige Werte tracken
    5. OnePager - Smooth Scrolling Navigation

    Viel Spaß beim Experimentieren! 🚀

    Weiterführende Links

    Comments: