Warum ein Rückgabewert die ganze Komponente neu rendert und der andere nicht
React Hook Form wirbt damit, unnötige Re-Renders zu vermeiden, indem es Eingabefelder unkontrolliert über Refs verwaltet statt über State. Genau dieser Vorteil verschwindet aber schnell wieder, sobald watch() ins Spiel kommt, denn der zurückgegebene Wert dieser Funktion löst bei jeder Eingabe einen Re-Render der gesamten Formular-Komponente aus, während der dafür vorgesehene useWatch()-Hook genau dieses Problem gezielt löst.
Inhaltsverzeichnis
- 1. Warum React Hook Form überhaupt unkontrollierte Felder nutzt
- 2. Wie watch() funktioniert und warum es die ganze Komponente betrifft
- 3. Wie useWatch() dasselbe Problem gezielt löst
- 4. Der Unterschied messbar machen
- 5. Wann watch() trotzdem in Ordnung ist
- 6. Weitere Varianten von useWatch() für konkrete Anwendungsfälle
- 7. useWatch() und Controller im Zusammenspiel
- 8. Bestehenden Code von watch() auf useWatch() migrieren
- 9. Die Entscheidung anhand der Formulargröße treffen
- 10. Zusammenfassung
- 11. FAQ
1. Warum React Hook Form überhaupt unkontrollierte Felder nutzt
React Hook Form unterscheidet sich von den meisten anderen Formular-Bibliotheken dadurch, dass Eingabefelder standardmäßig unkontrolliert bleiben: Statt jeden Tastendruck über useState in den React-State zu schreiben und dadurch bei jeder Eingabe neu zu rendern, registriert die Bibliothek Felder über Refs direkt am DOM-Element und liest deren Werte erst bei Bedarf aus, etwa beim Absenden des Formulars. Das ist der Hauptgrund, warum React Hook Form bei großen Formularen mit vielen Feldern spürbar performanter ist als State-basierte Alternativen.
Dieser Vorteil gilt aber nur, solange man nicht aktiv gegen ihn arbeitet. Sobald eine Komponente den aktuellen Wert eines Feldes während der Eingabe beobachten möchte, etwa um eine bedingte Anzeige zu steuern oder eine Live-Vorschau zu zeigen, muss React Hook Form diesen Wert notwendigerweise wieder in React-State überführen, damit ein Re-Render überhaupt ausgelöst werden kann. Genau an dieser Stelle entscheidet sich, ob nur die betroffene Stelle neu rendert oder die komplette Formular-Komponente, und genau hier unterscheiden sich watch() und useWatch() fundamental.
2. Wie watch() funktioniert und warum es die ganze Komponente betrifft
watch() ist eine Funktion, die vom useForm()-Hook zurückgegeben wird und beim Aufruf den aktuellen Wert eines oder mehrerer Felder liefert. Der entscheidende Punkt ist, wo dieser Aufruf typischerweise steht: direkt im Funktionskörper der Komponente, die auch useForm() aufgerufen hat. React Hook Form abonniert intern Änderungen an diesem Feld und löst bei jeder Änderung ein internes State-Update in genau dieser Komponente aus, um watch() beim nächsten Aufruf den aktuellen Wert liefern zu können.
Die Konsequenz: Jede Komponente, die watch() aufruft, rendert bei jeder Eingabe in das beobachtete Feld komplett neu, inklusive aller anderen Formularfelder, die in derselben Komponente gerendert werden. Bei einem kleinen Formular mit drei Feldern ist das kaum spürbar, bei einem umfangreichen Formular mit fünfzig Feldern, mehreren bedingten Abschnitten und aufwendig gerenderten Optionen kann jede einzelne Eingabe in ein einziges beobachtetes Feld die komplette Formular-Komponente inklusive aller unbeteiligten Felder neu rendern lassen.
function LargeForm() {
const { register, watch } = useForm();
// Jede Eingabe in "country" rendert die GESAMTE Komponente neu,
// inklusive aller unten registrierten Felder
const country = watch("country");
return (
<form>
<input {...register("country")} />
{country === "DE" && <input {...register("plz")} />}
<input {...register("firma")} />
<input {...register("strasse")} />
{/* ... 40 weitere Felder, die alle mit rendern ... */}
</form>
);
}
3. Wie useWatch() dasselbe Problem gezielt löst
useWatch() ist ein eigenständiger Hook, der denselben Zweck erfüllt wie watch(), den aktuellen Feldwert liefern, aber technisch anders funktioniert: Er abonniert die Feldänderung über einen eigenen internen Subscription-Mechanismus, der unabhängig von der Elternkomponente ist. Wird useWatch() in einer eigenen, kleinen Unterkomponente aufgerufen, dann löst eine Änderung des beobachteten Feldes nur einen Re-Render dieser kleinen Unterkomponente aus, nicht der übergeordneten Formular-Komponente mit allen ihren anderen Feldern.
Der Unterschied liegt also nicht im gelieferten Wert, beide geben den aktuellen Feldwert zurück, sondern ausschließlich darin, welche Komponente dabei neu rendert. Durch das Auslagern von useWatch() in eine dedizierte Komponente lässt sich der Re-Render-Radius exakt auf den Teil der Oberfläche begrenzen, der den beobachteten Wert tatsächlich anzeigt oder für eine bedingte Logik benötigt, während der Rest des Formulars komplett unberührt bleibt.
function LargeForm() {
const { register, control } = useForm();
return (
<form>
<input {...register("country")} />
{/* Nur CountryDependentField rendert bei Aenderung neu */}
<CountryDependentField control={control} />
<input {...register("firma")} />
<input {...register("strasse")} />
{/* ... 40 weitere Felder, die davon unberuehrt bleiben ... */}
</form>
);
}
function CountryDependentField({ control }) {
const country = useWatch({ control, name: "country" });
return country === "DE" ? <input name="plz" /> : null;
}
4. Der Unterschied messbar machen
Der Effekt lässt sich mit der bereits bekannten React-DevTools-Option Highlight updates when components render direkt sichtbar machen: Bei watch() blitzt bei jeder Eingabe die komplette Formular-Komponente einschließlich sämtlicher Kindfelder farbig auf, bei useWatch() in einer ausgelagerten Unterkomponente bleibt nur diese kleine Komponente sichtbar erleuchtet, während der Rest des Formulars stabil bleibt. Bei Formularen mit spürbarer Tipp-Verzögerung ist das oft der schnellste Weg, die Ursache einzugrenzen.
Für eine belastbare Aussage über die tatsächlichen Millisekunden lohnt sich zusätzlich eine kurze Profiler-Aufzeichnung während einer typischen Eingabesequenz. Bei Formularen mit rechenintensiv gerenderten Feldern, etwa einem Rich-Text-Editor, einer Kartenkomponente oder einer langen Select-Liste mit serverseitig gefilterten Optionen, kann der Unterschied zwischen beiden Varianten den Unterschied zwischen einem flüssigen und einem spürbar ruckelnden Formular ausmachen.
5. Wann watch() trotzdem in Ordnung ist
watch() ist keineswegs grundsätzlich zu vermeiden. Bei kleinen Formularen mit wenigen Feldern, bei denen ohnehin die gesamte Formular-Komponente überschaubar bleibt, ist der Unterschied zwischen watch() und useWatch() kaum messbar, und der direktere, kürzere Aufruf von watch() im Komponentenkörper spart unnötige Indirektion über eine eigene Unterkomponente. Auch für einmalige Lesevorgänge außerhalb des Render-Zyklus, etwa watch() ohne Argumente in einem Event-Handler zum Auslesen aller aktuellen Werte, entsteht gar kein zusätzliches Re-Render-Problem.
Problematisch wird watch() ausschließlich dann, wenn sein Rückgabewert direkt im Render-Pfad einer großen, aufwendig gerenderten Formular-Komponente verwendet wird und nur ein kleiner Teil der Oberfläche tatsächlich von diesem Wert abhängt. Die Faustregel lautet deshalb: Je größer das Formular und je teurer seine übrigen Felder in der Render-Kosten, desto eher lohnt sich die Auslagerung in useWatch() mit einer dedizierten Unterkomponente.
6. Weitere Varianten von useWatch() für konkrete Anwendungsfälle
useWatch() unterstützt neben der Beobachtung eines einzelnen Feldnamens auch ein Array von Feldnamen für mehrere gleichzeitig beobachtete Werte sowie den Aufruf ganz ohne name-Parameter, um das gesamte Formularobjekt zu beobachten, was für eine Live-Vorschau des kompletten Formularzustands nützlich ist. Zusätzlich lässt sich über defaultValue ein initialer Wert angeben, der genutzt wird, bevor das eigentliche Formular vollständig initialisiert ist, was Flackern beim ersten Rendern vermeidet.
Für den häufigen Fall, dass ein berechneter Wert aus mehreren Feldern abgeleitet werden soll, etwa eine Gesamtsumme aus mehreren Mengenfeldern, empfiehlt sich die Kombination aus useWatch() mit einem Array von Feldnamen und einer eigenen Unterkomponente, die ausschließlich diese Berechnung anzeigt. So bleibt die teure Neuberechnung und ihr Re-Render exakt auf die Anzeige der Summe begrenzt, während die einzelnen Eingabefelder selbst unkontrolliert und damit render-frei bleiben.
function TotalPreview({ control }) {
const [price, quantity] = useWatch({
control,
name: ["price", "quantity"],
});
const total = (Number(price) || 0) * (Number(quantity) || 0);
return <p>Gesamtsumme: {total.toFixed(2)} EUR</p>;
}
7. useWatch() und Controller im Zusammenspiel
Bei kontrollierten Drittanbieter-Komponenten, etwa einer Datepicker- oder Select-Bibliothek, die selbst einen value- und onChange-Prop erwartet, kommt in React Hook Form üblicherweise die Controller-Komponente zum Einsatz. Der intern von Controller verwendete Mechanismus ähnelt dabei useWatch(): Auch hier bleibt der Re-Render auf die jeweils registrierte kontrollierte Komponente begrenzt, statt die gesamte Formular-Komponente zu betreffen.
Wird zusätzlich der aktuelle Wert eines von Controller verwalteten Feldes an anderer Stelle im Formular benötigt, etwa für eine bedingte Anzeige, ist useWatch() mit demselben Feldnamen die richtige Ergänzung, statt den Wert erneut über watch() auf Komponentenebene abzufragen. So bleiben Controller und useWatch() konsistent im selben Isolationsprinzip, ohne dass ein Teil des Formulars versehentlich wieder die alte, teure watch()-Kopplung einführt.
8. Bestehenden Code von watch() auf useWatch() migrieren
Eine Migration lässt sich schrittweise durchführen, ohne das gesamte Formular auf einmal umzubauen: Zuerst identifiziert man mit dem DevTools-Highlighting, welche watch()-Aufrufe tatsächlich zu spürbaren Re-Renders der großen Formular-Komponente führen. Anschließend wird genau der Teil der Oberfläche, der vom beobachteten Wert abhängt, in eine eigene, kleine Komponente ausgelagert, die control als Prop erhält und useWatch() intern verwendet, statt den Wert von der Elternkomponente durchzureichen.
Wichtig ist dabei, control konsequent über Props oder den FormProvider-Kontext an die neue Unterkomponente weiterzugeben, statt useForm() dort ein zweites Mal aufzurufen, was zu einer inkonsistenten, unabhängigen Formularinstanz führen würde. Nach der Migration bestätigt ein erneuter Blick auf das Component Highlighting, dass nur noch die ausgelagerte Unterkomponente bei Eingaben aufblitzt, während der Rest des Formulars stabil bleibt.
9. Die Entscheidung anhand der Formulargröße treffen
Zusammengefasst hängt die richtige Wahl weniger von einer generellen Regel als von der konkreten Formulargröße und den Render-Kosten der übrigen Felder ab. Für Prototypen, kleine Formulare und einmalige Lesevorgänge außerhalb des Render-Pfads bleibt watch() die pragmatische, unkomplizierte Wahl, ohne unnötige Indirektion über zusätzliche Unterkomponenten einzuführen.
Für produktive Formulare mit vielen Feldern, bedingten Abschnitten oder rechenintensiven Kindkomponenten ist useWatch() in einer dedizierten Unterkomponente fast immer die überlegene Wahl, weil der Performance-Unterschied mit wachsender Formulargröße überproportional zunimmt. Die folgende Tabelle fasst diese Entscheidung noch einmal konkret zusammen.
| Kriterium | watch() | useWatch() | Empfehlung |
|---|---|---|---|
| Re-Render-Radius | Gesamte Komponente, die watch() aufruft | Nur die Komponente, die useWatch() aufruft | Bei großen Formularen useWatch() in Unterkomponente |
| Aufwand der Einrichtung | Direkter Aufruf im Komponentenkörper | Eigene Unterkomponente mit control-Prop nötig | Bei kleinen Formularen watch() bevorzugen |
| Mehrere Felder gleichzeitig | Array von Feldnamen als Argument möglich | Array von Feldnamen als name-Option möglich | Beide gleichwertig, Wahl hängt vom Render-Ort ab |
| Einmaliges Auslesen im Handler | Gut geeignet, kein Re-Render-Problem | Nicht dafür gedacht, ist ein reaktiver Hook | Für Event-Handler klar watch() verwenden |
Mironsoft
React-Architektur, Performance und Magento-Frontend-Integration
React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?
Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.
Performance-Audit
Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.
State-Architektur
Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.
Magento-Integration
GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.
10. Zusammenfassung
useWatch vs. watch(): Das Wichtigste auf einen Blick
watch()
Löst bei jeder Eingabe in das beobachtete Feld einen Re-Render der gesamten aufrufenden Komponente aus.
useWatch()
Isoliert den Re-Render auf die Komponente, in der der Hook selbst aufgerufen wird, unabhängig vom Elternteil.
Faustregel
Kleine Formulare vertragen watch(), große Formulare mit vielen Feldern brauchen useWatch() in Unterkomponenten.
Verifikation
React-DevTools-Highlighting zeigt sofort, ob nur die relevante Unterkomponente oder das ganze Formular aufblitzt.