Zum Inhalt springen

useRef

useState und useReducer verwalten Werte, deren Änderungen die Komponente neu rendern sollen. Manchmal braucht man aber einen Wert, der über Renders hinweg erhalten bleibt — ohne dass eine Änderung ein Re-Render auslöst. Und manchmal braucht man direkten Zugriff auf ein DOM-Element. Beides löst useRef.

useRef gibt ein Objekt mit einem einzigen Feld zurück: .current. Dieses Objekt bleibt über alle Renders hinweg dieselbe Referenz — React ersetzt es nie. Man kann .current jederzeit lesen und schreiben, ohne dass React davon erfährt.

import { useRef } from 'react'
const ref = useRef(0)
ref.current = ref.current + 1 // kein Re-Render
console.log(ref.current) // aktueller Wert

Das ist der fundamentale Unterschied zu useState: State-Änderungen lösen einen Re-Render aus und der neue Wert erscheint beim nächsten Render. Ref-Änderungen lösen keinen Re-Render aus — der Wert ist sofort da, aber React weiß nichts davon.

Ein häufiger Fall: eine setInterval-ID muss gespeichert werden, damit das Interval später gestoppt werden kann. Sie gehört nicht in State — ihre Änderung soll kein Re-Render auslösen:

import { useRef, useState } from 'react'
export default function Stoppuhr() {
const [sekunden, setSekunden] = useState(0)
const intervalRef = useRef<number | null>(null)
function starten() {
if (intervalRef.current !== null) return
intervalRef.current = setInterval(() => {
setSekunden(s => s + 1)
}, 1000)
}
function stoppen() {
clearInterval(intervalRef.current!)
intervalRef.current = null
}
return (
<div>
<p>{sekunden} Sekunden</p>
<button onClick={starten}>Start</button>
<button onClick={stoppen}>Stop</button>
</div>
)
}

sekunden gehört in State — die Anzeige soll sich aktualisieren. intervalRef gehört in einen Ref — die ID ist nur für clearInterval nötig, ihre Änderung soll nichts rendern.

Mit einem Ref lässt sich der Wert aus dem letzten Render festhalten:

import { useRef, useEffect, useState } from 'react'
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined)
useEffect(() => {
ref.current = value
})
return ref.current
}
export default function TodoZaehler({ anzahl }: { anzahl: number }) {
const vorige = usePrevious(anzahl)
return (
<p>
{anzahl} Todos
{vorige !== undefined && vorige !== anzahl && (
<span> (vorher: {vorige})</span>
)}
</p>
)
}

Der Effect läuft nach jedem Render — er schreibt den aktuellen Wert in den Ref, nachdem die Komponente gerendert hat. Beim nächsten Render liest usePrevious den Ref noch vor dem Effect — also den Wert vom letzten Render.

Manchmal muss man direkt mit einem DOM-Element interagieren: ein Eingabefeld fokussieren, ein Video starten, die Scrollposition lesen, eine nicht-React-Bibliothek anbinden. React bietet dafür das ref-Prop.

import { useRef } from 'react'
export default function SuchFormular() {
const inputRef = useRef<HTMLInputElement>(null)
function handleSuchen() {
inputRef.current?.focus()
}
return (
<div>
<input ref={inputRef} placeholder="Suche..." />
<button onClick={handleSuchen}>Fokus</button>
</div>
)
}

Wenn React das <input> in den DOM einfügt, schreibt es das echte DOM-Element in inputRef.current. Wenn die Komponente unmountet, setzt React inputRef.current wieder auf null.

import { useRef, useState } from 'react'
export default function TodoListe() {
const [todos, setTodos] = useState<string[]>([])
const listeRef = useRef<HTMLUListElement>(null)
function hinzufuegen(title: string) {
setTodos(prev => [...prev, title])
setTimeout(() => {
listeRef.current?.scrollTo({
top: listeRef.current.scrollHeight,
behavior: 'smooth',
})
}, 0)
}
return (
<ul ref={listeRef} style={{ maxHeight: 200, overflowY: 'auto' }}>
{todos.map((t, i) => <li key={i}>{t}</li>)}
</ul>
)
}

Das setTimeout(..., 0) stellt sicher, dass erst das DOM aktualisiert wird — dann erst wird gescrollt. Sauberer ist useEffect mit todos als Abhängigkeit — der Effect läuft garantiert nach dem DOM-Update:

import { useRef, useState, useEffect } from 'react'
export default function TodoListe() {
const [todos, setTodos] = useState<string[]>([])
const listeRef = useRef<HTMLUListElement>(null)
useEffect(() => {
listeRef.current?.scrollTo({
top: listeRef.current.scrollHeight,
behavior: 'smooth',
})
}, [todos])
function hinzufuegen(title: string) {
setTodos(prev => [...prev, title])
}
return (
<ul ref={listeRef} style={{ maxHeight: 200, overflowY: 'auto' }}>
{todos.map((t, i) => <li key={i}>{t}</li>)}
</ul>
)
}

useEffect(() => { ... }, [todos]) läuft jedes Mal, wenn sich todos ändert — immer nach dem Render, wenn das DOM bereits aktualisiert ist. Das [todos] ist das Dependency Array: es bestimmt, wann der Effect neu ausgeführt wird. Die vollständige Erklärung folgt im Kapitel useEffect: Daten laden.

Mutabler Wert: Der Typ des Initialwerts bestimmt ref.current:

const intervalRef = useRef<number | null>(null)
// ref.current: number | null
const zaehlerRef = useRef(0)
// ref.current: number — inferiert aus dem Initialwert

DOM-Ref: Der Generic gibt den erwarteten DOM-Elementtyp an, der Initialwert ist null:

const inputRef = useRef<HTMLInputElement>(null)
// ref.current: HTMLInputElement | null
const formRef = useRef<HTMLFormElement>(null)
const divRef = useRef<HTMLDivElement>(null)

ref.current ist null, bis React das Element in den DOM eingehängt hat — deshalb der optionale Chaining-Operator (?.) beim Zugriff.

useStateuseRef
Löst Re-Render ausjanein
Wert sichtbar beim Renderjanein — nur via .current
Geeignet fürDaten, die die UI steuernIDs, DOM-Elemente, Hilfswerte
Wert nach Render aktuelljaja — sofort

Faustregel: Wenn die Änderung eines Werts die Anzeige verändern soll, gehört er in State. Wenn der Wert nur intern gebraucht wird — als Hilfsgröße, ID oder DOM-Handle — gehört er in einen Ref.