Aller au contenu principal

Débogage React avec Cursor : Hooks, État et Performance

Cursor React Debugging

Les applications React peuvent présenter des bugs complexes liés à la gestion d'état, aux dépendances des hooks et à l'optimisation des rendus. Les capacités IA de Cursor peuvent vous aider à tracer le flux d'état, identifier les re-rendus inutiles et déboguer les problèmes de hooks. Ce guide couvre les scénarios de débogage React courants.

Prérequis​

Avant de commencer :

  • Cursor installé (dernière version recommandée)
  • Extension navigateur React DevTools
  • Familiarité avec les hooks React (useState, useEffect, useMemo)
  • Compréhension du cycle de rendu React

Configuration de l'environnement de débogage​

Paramètres Cursor recommandés​

// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.preferences.importModuleSpecifier": "relative",
"editor.quickSuggestions": {
"strings": true
}
}

Intégration de React DevTools​

  1. Installez React DevTools pour votre navigateur
  2. Ouvrez votre projet dans Cursor
  3. Utilisez l'onglet Components pour inspecter l'arbre des composants
  4. Utilisez l'onglet Profiler pour mesurer les performances de rendu

Débogage des Hooks React​

Problèmes de dépendances useEffect​

L'erreur la plus courante avec les hooks concerne les dépendances manquantes :

// ❌ Erreur : dépendance manquante
useEffect(() => {
fetchUser(userId);
}, []); // eslint-disable-line react-hooks/exhaustive-deps

Demandez à Cursor de corriger :

Cette useEffect a un avertissement de dépendance manquante. Corrige-le tout en évitant les boucles infinies.

Cursor suggère :

// ✅ Correct : inclut toutes les dépendances
useEffect(() => {
fetchUser(userId);
}, [userId]); // userId est maintenant inclus

Débogage des Hooks personnalisés​

// src/hooks/useLocalStorage.ts
import { useState, useEffect, useCallback } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T) {
// Initialisation paresseuse pour éviter d'accéder à localStorage à chaque rendu
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});

const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);

return [storedValue, setValue] as const;
}

Débogage du flux d'état​

Tracer le Prop Drilling​

Lorsque les données traversent plusieurs couches, utilisez Cursor pour refactoriser vers Context :

// ❌ Avant : Prop drilling
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}

function Layout({ user, setUser }) {
return <Header user={user} setUser={setUser} />;
}

// ✅ Après : Utilisation de Context
const UserContext = createContext(null);

function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}

Déboguer les consommateurs de Context​

// Utilisation de useContextSelector pour une souscription granulaire
import { createContext, useContext, useCallback } from 'react';

const ThemeContext = createContext(null);

export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

// Dans le composant
function ThemedButton() {
const { theme, toggleTheme } = useTheme();

// Re-render uniquement quand theme change, pas tout dans le contexte
return (
<button
style={{ background: theme.primary }}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}

Débogage des performances​

Identifier les rendus inutiles​

Utilisez le Profiler de React DevTools :

  1. Ouvrez l'onglet Profiler
  2. Cliquez sur Record (Enregistrer)
  3. Interagissez avec l'application
  4. Arrêtez l'enregistrement et analysez le graphique de flammes

Optimisation avec memo​

import { memo, useMemo, useCallback } from 'react';

// Envelopper le composant pour éviter les rendus inutiles
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
return (
<ul>
{items.map(item => (
<ListItem key={item.id} item={item} onSelect={onSelect} />
))}
</ul>
);
});

// Dans le composant parent
function Parent() {
const [items, setItems] = useState([]);
const [filter, setFilter] = useState('');

// Mémoriser la liste filtrée
const filteredItems = useMemo(
() => items.filter(item => item.name.includes(filter)),
[items, filter]
);

// Mémoriser le callback pour maintenir l'égalité de référence
const handleSelect = useCallback((id: string) => {
console.log('Selected:', id);
}, []);

return (
<ExpensiveList items={filteredItems} onSelect={handleSelect} />
);
}

Guide useMemo et useCallback​

HookUtilisé pourExemple
useMemoCalculs coûteuxFiltrer/trier de grands tableaux
useCallbackPasser des callbacks aux enfantsGestionnaires d'événements
memoOptimisation des composants pursRendu de grandes listes
Optimisation prématurée

Profilez les performances avant d'ajouter useMemo/useCallback. Cursor peut vous aider à identifier les véritables goulots d'étranglement.

Erreurs React courantes​

Boucles infinies​

Too many re-renders. React limits the number of renders to prevent an infinite loop.

Cause : Définition de l'état directement pendant le rendu

// ❌ Erreur : Définition de l'état pendant le rendu
function BadComponent() {
const [count, setCount] = useState(0);
setCount(count + 1); // Provoque une boucle infinie
return <div>{count}</div>;
}

// ✅ Correct : Définition de l'état dans le gestionnaire d'événements
function GoodComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}

Closure obsolète​

function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
// ❌ Erreur : 'count' est toujours 0
console.log(count);
setCount(count + 1);
}, 1000);

return () => clearInterval(interval);
}, []); // Dépendance count manquante

// ✅ Correct : Utilisation de la mise à jour fonctionnelle
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // Utilise toujours la valeur la plus récente
}, 1000);

return () => clearInterval(interval);
}, []);

return <div>{count}</div>;
}

Incompatibilité d'hydratation​

Warning: Text content did not match. Server: "..." Client: "..."

Utilisez Cursor pour corriger les problèmes SSR :

// Contenu dynamique rendu uniquement côté client
function ClientDate() {
const [date, setDate] = useState<string | null>(null);

useEffect(() => {
setDate(new Date().toLocaleDateString());
}, []);

if (!date) return <span>Loading...</span>;
return <span>{date}</span>;
}

Débogage des opérations asynchrones​

Pattern de récupération de données​

// src/hooks/useApi.ts
import { useState, useEffect, useRef } from 'react';

export function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const abortController = useRef<AbortController | null>(null);

useEffect(() => {
abortController.current = new AbortController();

async function fetchData() {
try {
setLoading(true);
setError(null);

const response = await fetch(url, {
signal: abortController.current?.signal,
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}

fetchData();

return () => {
abortController.current?.abort();
};
}, [url]);

return { data, loading, error };
}

Limites d'erreur avec Cursor​

// src/components/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
// Envoyer l'erreur au service de surveillance
}

render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div role="alert">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.message}</pre>
</details>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
);
}

return this.props.children;
}
}

Liste de contrôle rapide de débogage​

ProblèmeVérifier
Composant rendu trop souventReact DevTools Profiler
État ne se met pas à jourDépendances useEffect
Valeur de Context obsolèteRéférence de la valeur du Provider
Fuite mémoireFonction de nettoyage useEffect
Erreur d'hydratationDifférence de contenu client/serveur
Boucle infinieMise à jour d'état pendant le rendu

Ressources connexes​

Dernière mise à jour : Juillet 2025