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.
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:
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.
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!
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!
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;
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;
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.
Speicher mit useRef:
useRef<T | undefined>(undefined)
useRef
behält seinen Wert zwischen Renderings bei (im Gegensatz zu normalen Variablen)Timing-Trick mit useEffect:
useEffect
läuft nach jedem Rendering (da keine Abhängigkeiten angegeben sind)ref.current
mit dem aktuellen Wert von value
Der entscheidende Punkt:
ref.current
noch undefined
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;
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?"
useState ist richtig für:
useRef ist richtig für:
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}
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.
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!
Viel Spaß beim Experimentieren! 🚀