Die meisten React Hooks beeinflussen, was die Anwendung tut – State
verwalten, Effects ausführen, Werte memoizieren.
useDebugValue ist anders. Er beeinflusst nichts, was der
Endnutzer sieht. Er verändert kein Verhalten, löst keine Re-Renderings
aus, optimiert keine Performance. Seine einzige Aufgabe: Informationen
für Entwickler bereitstellen.
useDebugValue ist ein Entwicklungs-Tool. Er macht Custom
Hooks in den React DevTools transparent, indem er deren internen Zustand
annotiert. Für Library-Autoren, die wiederverwendbare Hooks schreiben,
und für Teams, die komplexe Hook-Logik kapseln, ist er unverzichtbar.
Für einfache Anwendungsfälle ist er überflüssig. Das macht ihn zum
seltensten genutzten Hook – aber auch zu einem der hilfreichsten, wenn
man ihn braucht.
Custom Hooks sind Funktionen, die andere Hooks aufrufen. Sie kapseln Logik, machen sie wiederverwendbar und halten Komponenten schlank. Aber diese Kapselung hat einen Nachteil: Von außen ist nicht sichtbar, was im Hook passiert.
function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Komplexe Auth-Logik
authService.getCurrentUser()
.then(setUser)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
const login = useCallback(async (credentials: Credentials) => {
// Login-Logik
}, []);
const logout = useCallback(() => {
// Logout-Logik
}, []);
return { user, loading, error, login, logout };
}
// Verwendung
function App() {
const auth = useAuth();
if (auth.loading) return <Spinner />;
return <div>Welcome {auth.user?.name}</div>;
}
Wenn man App in den React DevTools inspiziert, sieht man
den useAuth-Hook. Aber man sieht nicht was der
Hook gerade macht. Ist ein User eingeloggt? Welcher? Lädt gerade etwas?
Gibt es einen Fehler?
Man sieht die Hook-Struktur – useState #1, #2, #3,
useEffect, useCallback #1, #2 – aber nicht
deren semantische Bedeutung. Um zu verstehen, was der Hook macht, muss
man den Source Code lesen oder Breakpoints setzen.
Bei einfachen Hooks ist das kein Problem. Aber bei komplexen Hooks mit vielen internen States, Effects und berechneten Werten wird Debugging schwierig. Besonders in Third-Party-Libraries, wo man den Code vielleicht gar nicht direkt einsehen kann.
useDebugValue fügt Custom Hooks eine Annotation hinzu,
die in den React DevTools sichtbar wird. Es ist wie ein Label, das
erklärt, was der Hook gerade tut.
function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// DevTools-Annotation
useDebugValue(
user ? `Logged in as ${user.name}` : 'Not logged in'
);
// ... Rest der Logik
return { user, loading, error, login, logout };
}
Jetzt zeigen die DevTools unter useAuth die Annotation:
"Logged in as Alice" oder "Not logged in". Der
Hook ist nicht mehr eine Black Box, sondern dokumentiert seinen Zustand
selbst.
Die Annotation erscheint direkt unter dem Hook-Namen, nicht verschachtelt unter den internen Hooks. Das macht den Zustand auf einen Blick sichtbar.
useDebugValue hat eine minimalistische API. Ein
Parameter – der Wert, der angezeigt werden soll. Optional ein zweiter –
eine Formatierungsfunktion.
// Einfache Verwendung
useDebugValue(value);
// Mit Formatierungsfunktion
useDebugValue(value, formatter);
Der Wert kann alles sein – String, Number, Boolean, Object. Die
DevTools rendern ihn mit toString() bzw.
JSON-Serialisierung.
// String
useDebugValue('Loading...');
// Number
useDebugValue(42);
// Boolean
useDebugValue(true);
// Object
useDebugValue({ status: 'authenticated', role: 'admin' });
Für Objekte wird die JSON-Repräsentation angezeigt:
{"status":"authenticated","role":"admin"}. Das
funktioniert, ist aber nicht immer lesbar.
Die optionale Formatierungsfunktion ist ein cleveres Performance-Feature. Sie wird nur ausgeführt, wenn die React DevTools tatsächlich geöffnet sind. Wenn die DevTools geschlossen sind, wird die Funktion nicht aufgerufen. Kein Overhead, keine Berechnung.
Das ist wertvoll, wenn die Formatierung teuer ist – etwa wenn komplexe Objekte in lesbare Strings konvertiert werden müssen.
function useUserData(userId: string) {
const [userData, setUserData] = useState<ComplexUserData | null>(null);
// Teure Formatierung
useDebugValue(userData, data => {
if (!data) return 'No data';
// Komplexe Transformation
return `User: ${data.profile.name}, ` +
`Friends: ${data.friends.length}, ` +
`Last Active: ${new Date(data.lastActive).toLocaleDateString()}`;
});
return userData;
}
Die Formatierungsfunktion läuft nur, wenn ein Entwickler die DevTools öffnet und den Hook inspiziert. Für Production-Builds und normale Nutzung ist der Overhead null.
Die Funktion erhält den ersten Parameter als Input und gibt einen formatierten String zurück. Sie sollte pure sein – keine Side Effects, deterministisches Verhalten.
// ✓ Pure, deterministisch
useDebugValue(count, c => `Count: ${c}`);
// ❌ Side Effect
useDebugValue(count, c => {
console.log('Formatting!'); // Side Effect!
return `Count: ${c}`;
});
Hooks, die verschiedene Zustände verwalten, profitieren von klaren Status-Annotations.
function useDataFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Status als Annotation
useDebugValue(
error ? `Error: ${error.message}` :
loading ? 'Loading...' :
data ? 'Loaded' : 'Idle'
);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
Die DevTools zeigen: "Loading...", dann
"Loaded" oder "Error: Network timeout". Der
Hook-Zustand ist sofort klar.
Feature-Flags sind typischerweise in Hooks gekapselt. Die Annotation zeigt, welche Features aktiv sind.
function useFeatureFlag(flagName: string): boolean {
const [isEnabled, setIsEnabled] = useState(false);
useDebugValue(`${flagName}: ${isEnabled ? 'ON' : 'OFF'}`);
useEffect(() => {
// Feature-Flag von Server laden
featureFlagService.isEnabled(flagName)
.then(setIsEnabled);
}, [flagName]);
return isEnabled;
}
// Verwendung
function Component() {
const showNewUI = useFeatureFlag('new-ui');
const enableAnalytics = useFeatureFlag('analytics');
// DevTools zeigen:
// useFeatureFlag: "new-ui: ON"
// useFeatureFlag: "analytics: OFF"
}
Wenn ein Hook komplexe Berechnungen durchführt, kann die Annotation das Ergebnis zeigen.
function useShoppingCart() {
const [items, setItems] = useState<CartItem[]>([]);
const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
// Zusammenfassung als Annotation
useDebugValue(
{ items: itemCount, total: totalPrice },
({ items, total }) => `${items} items, $${total.toFixed(2)}`
);
return { items, totalPrice, itemCount, addItem, removeItem };
}
DevTools: "3 items, $47.50". Sofort sichtbar, ohne durch
interne States zu navigieren.
Auth-Hooks sind komplex und State-lastig. Eine klare Annotation hilft.
function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [role, setRole] = useState<'user' | 'admin' | null>(null);
useDebugValue(
user,
u => u
? `${u.name} (${u.email}) - Role: ${role}`
: 'Not authenticated'
);
// Auth-Logik
return { user, isAuthenticated, role, login, logout };
}
Hooks, die externe Verbindungen verwalten, profitieren von Status-Visibility.
function useWebSocket(url: string) {
const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
const [lastMessage, setLastMessage] = useState<any>(null);
const wsRef = useRef<WebSocket | null>(null);
useDebugValue(
{ status, url, lastMessage },
({ status, url, lastMessage }) =>
`${status.toUpperCase()} to ${url} - Last: ${lastMessage?.type || 'None'}`
);
useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => setStatus('connected');
ws.onclose = () => setStatus('disconnected');
ws.onmessage = (e) => setLastMessage(JSON.parse(e.data));
return () => ws.close();
}, [url]);
return { status, lastMessage, send: msg => wsRef.current?.send(msg) };
}
DevTools:
"CONNECTED to ws://api.example.com - Last: user_message".
useDebugValue ist nicht für jeden Hook sinnvoll. Die
Entscheidung hängt vom Kontext ab.
Verwenden Sie useDebugValue für:
Custom Hooks in Libraries: Wenn Sie eine Hook-Library veröffentlichen, machen Sie die Hooks debuggbar. Nutzer Ihrer Library werden es schätzen.
Komplexe Business-Logic-Hooks: Hooks, die viele interne States verwalten oder komplexe Berechnungen durchführen. Die Annotation macht transparent, was passiert.
Wiederverwendbare Team-Hooks: In großen Teams werden Hooks oft geteilt. Annotations helfen anderen Entwicklern zu verstehen, was der Hook macht, ohne den Source Code zu lesen.
Debugging-intensive Szenarien: Wenn ein Hook in der Vergangenheit schwer zu debuggen war oder oft zu Verwirrung führte, ist eine Annotation hilfreich.
Verwenden Sie useDebugValue NICHT für:
Triviale Hooks: Ein Hook, der nur
useState wrapped, braucht keine Annotation.
function useBoolean(initial) { const [value, setValue] = useState(initial); return [value, setValue]; }
– hier ist nichts zu annotieren.
Einmalige, lokale Hooks: Hooks, die nur in einer Komponente verwendet werden und dort inline definiert sind. Der Aufwand lohnt sich nicht.
Hooks ohne State: Hooks, die nur Utilities
kapseln ohne State zu verwalten. Beispiel:
function useDebounce(value, delay) { /* ... */ } – der Wert
ist bereits sichtbar.
Production-kritische Performance: In extrem performance-sensitiven Szenarien könnte selbst der minimale Overhead ein Problem sein. Aber das ist sehr selten.
| Kriterium | useDebugValue verwenden? |
|---|---|
| Library-Hook, öffentlich | ✓ Ja |
| Team-interner wiederverwendbarer Hook | ✓ Ja |
| Komplexer State oder Berechnungen | ✓ Ja |
| Lokaler, einmaliger Hook | ✗ Nein |
| Trivialer Wrapper um useState | ✗ Nein |
| Utility-Hook ohne State | ✗ Nein |
| Hook mit offensichtlichem Zustand | ✗ Optional |
useDebugValue ist ausschließlich ein Entwicklungs-Tool.
Er hat null Einfluss auf Production-Verhalten.
Missverständnis 1: useDebugValue für Logging
// ❌ Falsch: useDebugValue ist kein console.log
function useData() {
const [data, setData] = useState(null);
useDebugValue(data, d => {
console.log('Data changed:', d); // Läuft nur in DevTools!
return d;
});
return data;
}
Die Formatierungsfunktion läuft nur, wenn DevTools geöffnet sind. Für
Logging verwenden Sie useEffect oder direkte
console.log-Aufrufe.
Missverständnis 2: useDebugValue beeinflusst Re-Renderings
useDebugValue löst keine Re-Renderings aus. Es ist
passiv. Der Wert wird nur aktualisiert, wenn der Hook ohnehin neu
ausgeführt wird.
Missverständnis 3: useDebugValue in Komponenten
// ❌ Falsch: useDebugValue in Komponente
function Component() {
const [count, setCount] = useState(0);
useDebugValue(count); // Hat keine Wirkung!
return <div>{count}</div>;
}
useDebugValue funktioniert nur in Custom Hooks. In
normalen Komponenten hat es keine Wirkung. React ignoriert den Aufruf
stillschweigend.
Korrekte Verwendung:
// ✓ Richtig: useDebugValue in Custom Hook
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
useDebugValue(`Count: ${count}`);
return [count, setCount] as const;
}
function Component() {
const [count, setCount] = useCounter(); // Hook zeigt Annotation
return <div>{count}</div>;
}
TypeScript-Support für useDebugValue ist
straightforward. Der Hook akzeptiert jeden Typ für den Wert.
// Einfacher Wert
useDebugValue<string>('Loading');
// Komplexes Objekt
interface DebugInfo {
status: string;
count: number;
}
useDebugValue<DebugInfo>({ status: 'ready', count: 42 });
// Mit Formatter
useDebugValue<User | null, string>(
user,
u => u ? `User: ${u.name}` : 'No user'
);
Die Typisierung der Formatierungsfunktion ist besonders hilfreich. TypeScript inferiert den Input-Typ aus dem ersten Parameter und erwartet einen String als Return-Typ.
useDebugValue(
{ count: 5, active: true },
// TypeScript weiß: data ist { count: number; active: boolean }
data => `Count: ${data.count}, Active: ${data.active}`
);
1. Aussagekräftige, kurze Annotations
Die DevTools haben begrenzten Platz. Halten Sie Annotations prägnant.
// ❌ Zu verbose
useDebugValue(`The current authenticated user is ${user?.name} with email ${user?.email} and they have role ${role} and their last login was ${lastLogin}`);
// ✓ Kurz und klar
useDebugValue(user ? `${user.name} (${role})` : 'Not logged in');
2. Konsistente Formatierung
Wenn Sie mehrere Hooks mit ähnlichen Annotations haben, verwenden Sie konsistente Formate.
// Team-Convention: "Status - Details"
useDebugValue(`Authenticated - ${user.name}`);
useDebugValue(`Loading - ${progress}%`);
useDebugValue(`Error - ${error.message}`);
3. Bedingte Annotations
Passen Sie die Annotation an den Zustand an.
useDebugValue(
loading ? 'Loading...' :
error ? `Error: ${error.message}` :
data ? `Loaded ${data.items.length} items` :
'Idle'
);
4. Formatierungsfunktion für komplexe Objekte
Wenn der Wert ein komplexes Objekt ist, verwenden Sie eine Formatierungsfunktion für Lesbarkeit.
// Ohne Formatter: {"user":{"id":123,"name":"Alice"},"permissions":["read","write"]}
// Mit Formatter: "Alice (ID: 123) - Permissions: read, write"
useDebugValue(
authState,
state => `${state.user.name} (ID: ${state.user.id}) - ` +
`Permissions: ${state.permissions.join(', ')}`
);
Bringen wir alles zusammen in einem realistischen Custom Hook:
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodosState {
todos: Todo[];
loading: boolean;
error: string | null;
}
function useTodos(userId: string) {
const [state, setState] = useState<TodosState>({
todos: [],
loading: true,
error: null
});
// Debug-Annotation: Zeigt Status und Todo-Count
useDebugValue(
state,
s => {
if (s.loading) return 'Loading todos...';
if (s.error) return `Error: ${s.error}`;
const completed = s.todos.filter(t => t.completed).length;
const total = s.todos.length;
return `${completed}/${total} completed`;
}
);
useEffect(() => {
let ignore = false;
setState(s => ({ ...s, loading: true, error: null }));
fetch(`/api/users/${userId}/todos`)
.then(res => res.json())
.then(todos => {
if (!ignore) {
setState({ todos, loading: false, error: null });
}
})
.catch(err => {
if (!ignore) {
setState(s => ({ ...s, loading: false, error: err.message }));
}
});
return () => { ignore = true; };
}, [userId]);
const addTodo = useCallback((text: string) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text,
completed: false
};
setState(s => ({ ...s, todos: [...s.todos, newTodo] }));
}, []);
const toggleTodo = useCallback((id: string) => {
setState(s => ({
...s,
todos: s.todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
}));
}, []);
return {
todos: state.todos,
loading: state.loading,
error: state.error,
addTodo,
toggleTodo
};
}
// Verwendung
function TodoApp() {
const { todos, loading, addTodo, toggleTodo } = useTodos('user-123');
// In DevTools sichtbar:
// useTodos: "3/5 completed" (oder "Loading todos..." oder "Error: Network failed")
if (loading) return <Spinner />;
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />
))}
<AddTodoForm onAdd={addTodo} />
</div>
);
}
Die DevTools-Annotation zeigt sofort: Wie viele Todos gibt es? Wie viele sind erledigt? Lädt gerade etwas? Gibt es einen Fehler? Alles auf einen Blick, ohne durch State-Variablen zu navigieren.
useDebugValue ist kein Hook für jeden Tag. Aber wenn Sie
wiederverwendbare Hooks schreiben – sei es für ein Team, eine Library
oder einfach für sich selbst – macht er die Hooks transparent und
debuggbar. Eine kleine Annotation kann Stunden an Debugging-Zeit sparen,
wenn ein komplexer Hook nicht das tut, was er soll. Das macht
useDebugValue zu einem wertvollen Werkzeug im Arsenal jedes
Library-Autors und jeden Entwicklers, der wartbaren, professionellen
Code schreiben möchte.