React ist deklarativ. Wir beschreiben, was die UI zeigen soll, nicht wie sie es tun soll. Props fließen nach unten, Events nach oben. Komponenten sind Funktionen von Props zu UI. Dieses Modell ist elegant, vorhersagbar und hat React zu dem gemacht, was es ist.
Und dann gibt es useImperativeHandle – einen Hook, der
bewusst gegen dieses Paradigma verstößt. Er erlaubt Parent-Komponenten,
Methoden auf Child-Komponenten aufzurufen. Direkt. Imperativ. Das fühlt
sich zunächst falsch an, und das ist gut so. Diese Unbehaglichkeit ist
ein Warnsignal: Hier verlassen wir sicheres Terrain.
Aber manchmal ist genau das notwendig. Das Web ist fundamentally
imperativ. DOM-APIs wie focus(), play(),
scrollIntoView() sind Befehle, keine
Zustandsbeschreibungen. Es gibt keinen deklarativen Weg zu sagen “dieses
Element ist fokussiert” oder “dieses Video spielt gerade”. Diese
Operationen sind Aktionen, und Aktionen erfordern imperative
Kontrolle.
useImperativeHandle ist React’s Antwort auf dieses
Problem. Kein Allheilmittel, sondern ein gezieltes Werkzeug für
spezifische Szenarien, wo die deklarative Abstraktion bröckelt.
Stellen wir uns eine Custom-Input-Komponente vor, die komplexe Validierung, Formatierung und verschiedene Zustände verwaltet. Die Parent-Komponente möchte, dass dieses Input bei einem bestimmten Event fokussiert wird – etwa wenn ein Formular-Fehler auftritt.
Der naive Ansatz könnte so aussehen:
// ❌ Funktioniert nicht
function CustomInput({ shouldFocus }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (shouldFocus) {
inputRef.current?.focus();
}
}, [shouldFocus]);
return <input ref={inputRef} />;
}
// Parent muss State verwalten
function Form() {
const [shouldFocus, setShouldFocus] = useState(false);
const handleError = () => {
setShouldFocus(true);
// Problem: Wie resetten wir shouldFocus?
};
}
Das funktioniert einmal. Aber wie fokussiert man das Input ein
zweites Mal? shouldFocus ist bereits true. Man
müsste es auf false setzen, einen Render-Cycle warten, dann
wieder auf true. Das ist umständlich und
fehleranfällig.
Das eigentliche Problem: Wir versuchen, eine Aktion als Zustand zu modellieren. Fokus ist kein Zustand, sondern ein Befehl. “Fokussiere jetzt!” ist imperativ, nicht deklarativ.
useImperativeHandle schafft eine Brücke. Er erlaubt es
einer Komponente, eine imperative API zu definieren, die über eine Ref
zugänglich ist. Die Parent-Komponente erhält keine volle Kontrolle über
die Child-Komponente, sondern nur Zugriff auf explizit bereitgestellte
Methoden.
Das Pattern besteht aus drei Teilen:
1. TypeScript-Interface für die API
Definiert, welche Methoden nach außen sichtbar sind.
interface CustomInputHandle {
focus: () => void;
reset: () => void;
getValue: () => string;
}
2. forwardRef für Ref-Weiterleitung
forwardRef erlaubt es einer Komponente, eine Ref von
ihrem Parent zu empfangen. Normalerweise können Funktionskomponenten
keine Refs empfangen – forwardRef macht sie ref-fähig.
const CustomInput = forwardRef<CustomInputHandle, Props>((props, ref) => {
// Komponenten-Logik
});
3. useImperativeHandle zum Definieren der API
Innerhalb der Komponente definiert useImperativeHandle,
was über die Ref zugänglich ist.
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
reset: () => setValue(''),
getValue: () => value
}), [value]);
Die vollständige Implementierung:
interface CustomInputHandle {
focus: () => void;
reset: () => void;
getValue: () => string;
}
interface CustomInputProps {
defaultValue?: string;
onValueChange?: (value: string) => void;
}
const CustomInput = forwardRef<CustomInputHandle, CustomInputProps>(
({ defaultValue = '', onValueChange }, ref) => {
const [value, setValue] = useState(defaultValue);
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
reset: () => {
setValue('');
onValueChange?.('');
},
getValue: () => value
}), [value, onValueChange]);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
onValueChange?.(newValue);
};
return <input ref={inputRef} value={value} onChange={handleChange} />;
}
);
Die Parent-Komponente kann jetzt imperative Methoden aufrufen:
function Form() {
const inputRef = useRef<CustomInputHandle>(null);
const handleError = () => {
inputRef.current?.focus();
};
const handleReset = () => {
inputRef.current?.reset();
};
const handleSubmit = () => {
const value = inputRef.current?.getValue();
console.log('Submitted:', value);
};
return (
<div>
<CustomInput ref={inputRef} onValueChange={v => console.log(v)} />
<button onClick={handleError}>Focus</button>
<button onClick={handleReset}>Reset</button>
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
Die Zusammenarbeit zwischen forwardRef und
useImperativeHandle ist essentiell. Ohne
forwardRef kann eine Funktionskomponente keine Ref
empfangen. forwardRef erweitert die Komponenten-Signatur um
einen zweiten Parameter – die Ref.
// Normale Komponente
function Component(props: Props) { /* ... */ }
// Mit forwardRef
const Component = forwardRef<RefType, Props>((props, ref) => { /* ... */ });
Der generische Typ-Parameter ist wichtig. Der erste
(RefType) definiert den Typ der Ref – also was über
ref.current zugänglich ist. Der zweite (Props)
definiert die Props der Komponente.
useImperativeHandle nimmt drei Parameter:
useEffect oder
useMemouseImperativeHandle(
ref, // Die Ref
() => ({ // Factory-Funktion
focus: () => { /* ... */ },
reset: () => { /* ... */ }
}),
[dependencies] // Dependencies
);
Die Factory-Funktion wird bei jedem Rendering ausgeführt, wenn sich
Dependencies ändern. Das API-Objekt wird dann auf
ref.current gesetzt. Die Dependencies sind wichtig – wenn
die Methoden auf State oder Props zugreifen, müssen diese gelistet
sein.
// ❌ Falsch: value fehlt in Dependencies
useImperativeHandle(ref, () => ({
getValue: () => value // Closure über value
}), []); // value nicht gelistet!
// ✓ Richtig: value in Dependencies
useImperativeHandle(ref, () => ({
getValue: () => value
}), [value]);
Ohne korrekte Dependencies arbeiten die Methoden mit stale values – veralteten Werten aus früheren Renderings.
useImperativeHandle ist für spezifische Szenarien
gedacht. Die meisten dieser Szenarien involvieren DOM-Operationen, die
keine deklarative Entsprechung haben.
Der klassische Fall. Accessibility-Richtlinien erfordern oft programmatische Fokus-Kontrolle.
interface ModalHandle {
focus: () => void;
close: () => void;
}
const Modal = forwardRef<ModalHandle, ModalProps>(
({ children, onClose }, ref) => {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
closeButtonRef.current?.focus();
},
close: () => {
onClose();
}
}), [onClose]);
return (
<div ref={modalRef} role="dialog">
{children}
<button ref={closeButtonRef} onClick={onClose}>
Close
</button>
</div>
);
}
);
// Verwendung
function App() {
const modalRef = useRef<ModalHandle>(null);
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
// Nach Rendering: Focus auf Modal
setTimeout(() => modalRef.current?.focus(), 0);
};
return (
<>
<button onClick={openModal}>Open Modal</button>
{isOpen && (
<Modal ref={modalRef} onClose={() => setIsOpen(false)}>
Modal Content
</Modal>
)}
</>
);
}
Video und Audio haben imperative APIs. play(),
pause(), seek() sind Befehle.
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
getCurrentTime: () => number;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
({ src, onTimeUpdate }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current?.play();
},
pause: () => {
videoRef.current?.pause();
},
seek: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
getCurrentTime: () => {
return videoRef.current?.currentTime ?? 0;
}
}), []);
return (
<video
ref={videoRef}
src={src}
onTimeUpdate={e => onTimeUpdate?.(e.currentTarget.currentTime)}
/>
);
}
);
Scroll ist eine Aktion, kein Zustand. scrollIntoView(),
scrollTo() sind imperative DOM-Methoden.
interface ScrollableListHandle {
scrollToTop: () => void;
scrollToBottom: () => void;
scrollToItem: (index: number) => void;
}
const ScrollableList = forwardRef<ScrollableListHandle, ListProps>(
({ items }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
useImperativeHandle(ref, () => ({
scrollToTop: () => {
containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
},
scrollToBottom: () => {
containerRef.current?.scrollTo({
top: containerRef.current.scrollHeight,
behavior: 'smooth'
});
},
scrollToItem: (index: number) => {
itemRefs.current[index]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}), []);
return (
<div ref={containerRef} style={{ height: 400, overflow: 'auto' }}>
{items.map((item, i) => (
<div key={item.id} ref={el => itemRefs.current[i] = el}>
{item.text}
</div>
))}
</div>
);
}
);
JavaScript-Animationen erfordern oft imperative Steuerung.
interface AnimatedBoxHandle {
startAnimation: () => void;
stopAnimation: () => void;
resetAnimation: () => void;
}
const AnimatedBox = forwardRef<AnimatedBoxHandle, AnimatedBoxProps>(
({ children }, ref) => {
const boxRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<Animation | null>(null);
useImperativeHandle(ref, () => ({
startAnimation: () => {
if (!boxRef.current) return;
animationRef.current = boxRef.current.animate([
{ transform: 'translateX(0px)' },
{ transform: 'translateX(300px)' }
], {
duration: 1000,
easing: 'ease-in-out'
});
},
stopAnimation: () => {
animationRef.current?.pause();
},
resetAnimation: () => {
animationRef.current?.cancel();
}
}), []);
return <div ref={boxRef}>{children}</div>;
}
);
Die Versuchung ist groß, useImperativeHandle als
Allzweck-Tool zu missbrauchen. Aber für die meisten Szenarien gibt es
bessere, deklarative Alternativen.
Nicht für State-Management verwenden
Ein häufiger Fehler ist der Versuch, State über imperative Methoden zu verwalten.
// ❌ Falsch: State via imperative API
interface CounterHandle {
increment: () => void;
decrement: () => void;
setValue: (value: number) => void;
}
const Counter = forwardRef<CounterHandle, CounterProps>((props, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
setValue: (value) => setCount(value)
}), []);
return <div>{count}</div>;
});
// ✓ Richtig: State via Props
interface CounterProps {
value: number;
onIncrement: () => void;
onDecrement: () => void;
onValueChange: (value: number) => void;
}
function Counter({ value, onIncrement, onDecrement }: CounterProps) {
return (
<div>
{value}
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
State sollte über Props fließen. Die Parent-Komponente verwaltet den State, die Child-Komponente rendert ihn. Das ist React’s Kern-Paradigma.
Nicht als Ersatz für Callbacks
// ❌ Falsch: Callback via imperative API
interface FormHandle {
onSubmit: (data: FormData) => void;
}
// ✓ Richtig: Callback als Prop
interface FormProps {
onSubmit: (data: FormData) => void;
}
Callbacks sind bereits imperativ – die Parent-Komponente übergibt
eine Funktion, die Child ruft sie auf. Kein Grund für
useImperativeHandle.
Nicht für einfachen DOM-Zugriff
Wenn Sie nur DOM-Referenzen benötigen, brauchen Sie keinen
useImperativeHandle.
// ❌ Überkomplex
interface InputHandle {
getElement: () => HTMLInputElement | null;
}
const Input = forwardRef<InputHandle>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
getElement: () => inputRef.current
}), []);
return <input ref={inputRef} />;
});
// ✓ Einfacher: Direkte Ref-Weiterleitung
const Input = forwardRef<HTMLInputElement>((props, ref) => {
return <input ref={ref} />;
});
forwardRef kann Refs direkt an DOM-Elemente
weiterleiten. Kein useImperativeHandle nötig.
useImperativeHandle hat Implikationen für die
Architektur. Er erzeugt eine bidirektionale Abhängigkeit zwischen Parent
und Child – der Parent kennt die Child-API, die Child weiß, dass sie
aufgerufen werden kann.
| Aspekt | Deklarativ (Props) | Imperativ (useImperativeHandle) |
|---|---|---|
| Datenfluss | Unidirektional (Props down, Events up) | Bidirektional (Props down, Methodenaufrufe down) |
| Testbarkeit | Einfach (Props setzen, Output prüfen) | Komplexer (Refs mocken, Methoden aufrufen) |
| Debugging | Klar (Props-Flow in DevTools) | Schwieriger (Imperative Aufrufe nicht sichtbar) |
| Wiederverwendbarkeit | Hoch (Komponente ist selbstständig) | Niedriger (Erwartung an Parent-Steuerung) |
| Vorhersagbarkeit | Hoch (Gleiche Props → Gleiches Rendering) | Niedriger (Methoden können jederzeit aufgerufen werden) |
Die Entscheidung für useImperativeHandle sollte bewusst
getroffen werden. Fragen Sie sich:
Wenn die Antworten “Ja”, “Nein”, “Ja” sind, ist
useImperativeHandle wahrscheinlich die richtige Wahl.
1. Minimale API-Oberfläche
Exponieren Sie nur, was wirklich nötig ist. Jede Methode in der API ist ein Vertrag, der gepflegt werden muss.
// ❌ Zu viel exponiert
useImperativeHandle(ref, () => ({
focus, blur, select, setSelectionRange, getValue, setValue,
reset, validate, clearError, setError, ...
}), [/* ... */]);
// ✓ Minimal, fokussiert
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
reset: () => setValue('')
}), []);
2. Dokumentation in TypeScript
Nutzen Sie JSDoc für API-Dokumentation.
interface FormHandle {
/** Fokussiert das erste Input-Feld mit Fehler */
focusFirstError: () => void;
/** Setzt alle Felder auf ihre Default-Werte zurück */
reset: () => void;
/** Gibt die aktuellen Form-Werte zurück */
getValues: () => FormValues;
}
3. Defensive Programmierung
Gehen Sie davon aus, dass Methoden zu ungünstigen Zeitpunkten aufgerufen werden.
useImperativeHandle(ref, () => ({
focus: () => {
if (!inputRef.current) {
console.warn('Cannot focus: ref not attached');
return;
}
inputRef.current.focus();
}
}), []);
4. Kombinieren Sie mit useCallback
Wenn Methoden komplex sind, memoizieren Sie sie.
const handleFocus = useCallback(() => {
// Komplexe Logik
}, [dependencies]);
useImperativeHandle(ref, () => ({
focus: handleFocus
}), [handleFocus]);
useImperativeHandle ist ein Spezialwerkzeug für
spezielle Probleme. Er ist nicht Teil des normalen React-Workflows,
sondern die Ausnahme – der Notausgang, wenn deklarative Patterns nicht
ausreichen. Richtig eingesetzt, ermöglicht er elegante Lösungen für
DOM-bezogene Operationen. Falsch eingesetzt, führt er zu schwer
wartbarem Code, der React’s Kernprinzipien untergräbt. Die Kunst liegt
darin, die Grenze zu erkennen.