28 useDebugValue – Custom Hooks dokumentieren

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.

28.1 Das Problem: Custom Hooks als Black Boxes

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.

28.2 Die Lösung: Zustand annotieren

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.

28.3 Die API: Einfach und fokussiert

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.

28.4 Die Formatierungsfunktion: Performance-bewusste Annotation

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}`;
});

28.5 Praktische Patterns

28.5.1 Pattern 1: Status-Annotation für State-Hooks

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.

28.5.2 Pattern 2: Feature-Toggle-Hooks

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"
}

28.5.3 Pattern 3: Komplexe berechnete Werte

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.

28.5.4 Pattern 4: Authentifizierungs-Hooks

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 };
}

28.5.5 Pattern 5: WebSocket-Verbindungen

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".

28.6 Wann verwenden, wann nicht

useDebugValue ist nicht für jeden Hook sinnvoll. Die Entscheidung hängt vom Kontext ab.

Verwenden Sie useDebugValue für:

Verwenden Sie useDebugValue NICHT für:

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

28.7 Einschränkungen und Missverständnisse

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>;
}

28.8 Integration mit TypeScript

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}`
);

28.9 Best Practices

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(', ')}`
);

28.10 Ein vollständiges Beispiel

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.