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

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
- Installez React DevTools pour votre navigateur
- Ouvrez votre projet dans Cursor
- Utilisez l'onglet Components pour inspecter l'arbre des composants
- 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 :
- Ouvrez l'onglet Profiler
- Cliquez sur Record (Enregistrer)
- Interagissez avec l'application
- 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
| Hook | Utilisé pour | Exemple |
|---|---|---|
useMemo | Calculs coûteux | Filtrer/trier de grands tableaux |
useCallback | Passer des callbacks aux enfants | Gestionnaires d'événements |
memo | Optimisation des composants purs | Rendu de grandes listes |
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ème | Vérifier |
|---|---|
| Composant rendu trop souvent | React DevTools Profiler |
| État ne se met pas à jour | Dépendances useEffect |
| Valeur de Context obsolète | Référence de la valeur du Provider |
| Fuite mémoire | Fonction de nettoyage useEffect |
| Erreur d'hydratation | Différence de contenu client/serveur |
| Boucle infinie | Mise à jour d'état pendant le rendu |
Ressources connexes
Dernière mise à jour : Juillet 2025