Libérez la puissance des hooks personnalisés React et de la composition d'effets pour gérer les effets de bord complexes. Apprenez à les orchestrer pour un code plus propre.
Composition d'effets avec les hooks personnalisés React : Maîtriser l'orchestration d'effets complexes
Les hooks personnalisés de React ont révolutionné la façon dont nous gérons la logique d'état et les effets de bord dans nos applications. Bien que useEffect
soit un outil puissant, les composants complexes peuvent rapidement devenir difficiles à gérer avec de multiples effets entrelacés. C'est là qu'intervient la composition d'effets – une technique qui nous permet de décomposer les effets complexes en hooks personnalisés plus petits et réutilisables, aboutissant à un code plus propre et plus facile à maintenir.
Qu'est-ce que la composition d'effets ?
La composition d'effets est la pratique consistant à combiner plusieurs effets plus petits, généralement encapsulés dans des hooks personnalisés, pour créer un effet plus grand et plus complexe. Au lieu de concentrer toute la logique dans un seul appel à useEffect
, nous créons des unités de fonctionnalités réutilisables qui peuvent être composées ensemble selon les besoins. Cette approche favorise la réutilisabilité du code, améliore la lisibilité et simplifie les tests.
Pourquoi utiliser la composition d'effets ?
Il existe plusieurs raisons impérieuses d'adopter la composition d'effets dans vos projets React :
- Réutilisabilité du code améliorée : Les hooks personnalisés peuvent être réutilisés dans plusieurs composants, réduisant la duplication de code et améliorant la maintenabilité.
- Lisibilité accrue : Décomposer les effets complexes en unités plus petites et ciblées rend le code plus facile à comprendre et à raisonner.
- Tests simplifiés : Les effets plus petits et isolés sont plus faciles à tester et à déboguer.
- Modularité augmentée : La composition d'effets favorise une architecture modulaire, facilitant l'ajout, la suppression ou la modification de fonctionnalités sans affecter d'autres parties de l'application.
- Complexité réduite : Gérer un grand nombre d'effets de bord dans un seul
useEffect
peut conduire à du code spaghetti. La composition d'effets aide à décomposer la complexité en morceaux gérables.
Exemple de base : Combiner la récupération de données et la persistance dans le Local Storage
Considérons un scénario où nous devons récupérer les données d'un utilisateur depuis une API et les conserver dans le stockage local. Sans la composition d'effets, nous pourrions nous retrouver avec un seul useEffect
gérant les deux tâches. Voici comment nous pouvons obtenir le même résultat avec la composition d'effets :
1. Création du hook useFetchData
Ce hook est responsable de la récupération des données depuis une API.
import { useState, useEffect } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetchData;
2. Création du hook useLocalStorage
Ce hook gère la persistance des données dans le stockage local.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
export default useLocalStorage;
3. Composer les hooks dans un composant
Maintenant, nous pouvons composer ces hooks dans un composant pour récupérer les données utilisateur et les conserver dans le stockage local.
import React from 'react';
import useFetchData from './useFetchData';
import useLocalStorage from './useLocalStorage';
function UserProfile() {
const { data: userData, loading, error } = useFetchData('https://api.example.com/user/profile');
const [storedUserData, setStoredUserData] = useLocalStorage('userProfile', null);
useEffect(() => {
if (userData) {
setStoredUserData(userData);
}
}, [userData, setStoredUserData]);
if (loading) {
return Chargement du profil utilisateur...
;
}
if (error) {
return Erreur lors de la récupération du profil utilisateur : {error.message}
;
}
if (!userData && !storedUserData) {
return Aucune donnée utilisateur disponible.
;
}
const userToDisplay = storedUserData || userData;
return (
Profil Utilisateur
Nom : {userToDisplay.name}
Email : {userToDisplay.email}
);
}
export default UserProfile;
Dans cet exemple, nous avons séparé la logique de récupération des données et la logique de persistance dans le stockage local en deux hooks personnalisés distincts. Le composant UserProfile
compose ensuite ces hooks pour atteindre la fonctionnalité désirée. Cette approche rend le code plus modulaire, réutilisable et plus facile à tester.
Exemples avancés : Orchestrer des effets complexes
La composition d'effets devient encore plus puissante face à des scénarios plus complexes. Explorons quelques exemples avancés.
1. Gérer les abonnements et les écouteurs d'événements
Imaginez un scénario où vous devez vous abonner à un WebSocket et écouter des événements spécifiques. Vous devez également gérer le nettoyage lorsque le composant est démonté. Voici comment vous pouvez utiliser la composition d'effets pour gérer cela :
a. Création du hook useWebSocket
Ce hook établit une connexion WebSocket et gère la logique de reconnexion.
import { useState, useEffect, useRef } from 'react';
function useWebSocket(url) {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const retryCount = useRef(0);
useEffect(() => {
const connect = () => {
const newSocket = new WebSocket(url);
newSocket.onopen = () => {
console.log('WebSocket connecté');
setIsConnected(true);
retryCount.current = 0;
};
newSocket.onclose = () => {
console.log('WebSocket déconnecté');
setIsConnected(false);
// Backoff exponentiel pour la reconnexion
const timeout = Math.min(3000 * Math.pow(2, retryCount.current), 60000);
retryCount.current++;
console.log(`Reconnexion dans ${timeout/1000} secondes...`);
setTimeout(connect, timeout);
};
newSocket.onerror = (error) => {
console.error('Erreur WebSocket :', error);
};
setSocket(newSocket);
};
connect();
return () => {
if (socket) {
socket.close();
}
};
}, [url]);
return { socket, isConnected };
}
export default useWebSocket;
b. Création du hook useEventListener
Ce hook vous permet d'écouter facilement des événements spécifiques sur le WebSocket.
import { useEffect } from 'react';
function useEventListener(socket, eventName, handler) {
useEffect(() => {
if (!socket) return;
const listener = (event) => handler(event);
socket.addEventListener(eventName, listener);
return () => {
socket.removeEventListener(eventName, listener);
};
}, [socket, eventName, handler]);
}
export default useEventListener;
c. Composer les hooks dans un composant
import React, { useState } from 'react';
import useWebSocket from './useWebSocket';
import useEventListener from './useEventListener';
function WebSocketComponent() {
const { socket, isConnected } = useWebSocket('wss://echo.websocket.events');
const [message, setMessage] = useState('');
const [receivedMessages, setReceivedMessages] = useState([]);
useEventListener(socket, 'message', (event) => {
setReceivedMessages((prevMessages) => [...prevMessages, event.data]);
});
const sendMessage = () => {
if (socket && isConnected) {
socket.send(message);
setMessage('');
}
};
return (
Exemple WebSocket
Statut de la connexion : {isConnected ? 'Connecté' : 'Déconnecté'}
setMessage(e.target.value)}
placeholder="Saisir un message"
/>
Messages reçus :
{receivedMessages.map((msg, index) => (
- {msg}
))}
);
}
export default WebSocketComponent;
Dans cet exemple, useWebSocket
gère la connexion WebSocket, y compris la logique de reconnexion, tandis que useEventListener
fournit un moyen propre de s'abonner à des événements spécifiques. Le composant WebSocketComponent
compose ces hooks pour créer un client WebSocket entièrement fonctionnel.
2. Orchestrer les opérations asynchrones avec des dépendances
Parfois, les effets doivent être déclenchés dans un ordre spécifique ou en fonction de certaines dépendances. Disons que vous devez récupérer les données d'un utilisateur, puis récupérer ses publications en fonction de son ID, et enfin mettre à jour l'interface utilisateur. Vous pouvez utiliser la composition d'effets pour orchestrer ces opérations asynchrones.
a. Création du hook useUserData
Ce hook récupère les données de l'utilisateur.
import { useState, useEffect } from 'react';
function useUserData(userId) {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setUserData(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [userId]);
return { userData, loading, error };
}
export default useUserData;
b. Création du hook useUserPosts
Ce hook récupère les publications d'un utilisateur en fonction de son ID.
import { useState, useEffect } from 'react';
function useUserPosts(userId) {
const [userPosts, setUserPosts] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId) {
setUserPosts(null);
setLoading(false);
return;
}
const fetchPosts = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setUserPosts(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, [userId]);
return { userPosts, loading, error };
}
export default useUserPosts;
c. Composer les hooks dans un composant
import React, { useState } from 'react';
import useUserData from './useUserData';
import useUserPosts from './useUserPosts';
function UserProfileWithPosts() {
const [userId, setUserId] = useState(1); // Commencer avec un ID utilisateur par défaut
const { userData, loading: userLoading, error: userError } = useUserData(userId);
const { userPosts, loading: postsLoading, error: postsError } = useUserPosts(userId);
return (
Profil utilisateur avec publications
setUserId(parseInt(e.target.value, 10))}
/>
{userLoading ? Chargement des données utilisateur...
: null}
{userError ? Erreur lors du chargement des données utilisateur : {userError.message}
: null}
{userData ? (
Détails de l'utilisateur
Nom : {userData.name}
Email : {userData.email}
) : null}
{postsLoading ? Chargement des publications de l'utilisateur...
: null}
{postsError ? Erreur lors du chargement des publications de l'utilisateur : {postsError.message}
: null}
{userPosts ? (
Publications de l'utilisateur
{userPosts.map((post) => (
- {post.title}
))}
) : null}
);
}
export default UserProfileWithPosts;
Dans cet exemple, useUserPosts
dépend de l'userId
. Le hook ne récupère les publications que lorsqu'un userId
valide est disponible. Cela garantit que les effets sont déclenchés dans le bon ordre et que l'interface utilisateur est mise à jour en conséquence.
Meilleures pratiques pour la composition d'effets
Pour tirer le meilleur parti de la composition d'effets, tenez compte des meilleures pratiques suivantes :
- Principe de responsabilité unique : Chaque hook personnalisé doit avoir une seule responsabilité bien définie.
- Noms descriptifs : Utilisez des noms descriptifs pour vos hooks personnalisés afin d'indiquer clairement leur objectif.
- Tableaux de dĂ©pendances : GĂ©rez soigneusement les tableaux de dĂ©pendances dans vos appels Ă
useEffect
pour éviter les rendus inutiles ou les boucles infinies. - Tests : Rédigez des tests unitaires pour vos hooks personnalisés afin de vous assurer qu'ils se comportent comme prévu.
- Documentation : Documentez vos hooks personnalisés pour les rendre plus faciles à comprendre et à réutiliser.
- Évitez la sur-abstraction : N'abusez pas de l'ingénierie pour vos hooks personnalisés. Gardez-les simples et ciblés.
- Pensez à la gestion des erreurs : Mettez en œuvre une gestion robuste des erreurs dans vos hooks personnalisés pour gérer gracieusement les situations inattendues.
Considérations globales
Lors du développement d'applications React pour un public mondial, gardez à l'esprit les considérations suivantes :
- Internationalisation (i18n) : Utilisez une bibliothèque comme
react-intl
oui18next
pour prendre en charge plusieurs langues. - Localisation (l10n) : Adaptez votre application aux différentes préférences régionales, telles que les formats de date et de nombre.
- Accessibilité (a11y) : Assurez-vous que votre application est accessible aux utilisateurs handicapés en suivant les directives WCAG.
- Performance : Optimisez votre application pour différentes conditions de réseau et capacités d'appareils. Envisagez d'utiliser des techniques comme le fractionnement de code (code splitting) et le chargement paresseux (lazy loading).
- Réseaux de diffusion de contenu (CDN) : Utilisez un CDN pour distribuer les actifs de votre application à partir de serveurs situés plus près de vos utilisateurs, réduisant ainsi la latence et améliorant les performances.
- Fuseaux horaires : Lorsque vous traitez des dates et des heures, soyez attentif aux différents fuseaux horaires et utilisez des bibliothèques appropriées comme
moment-timezone
oudate-fns-timezone
.
Exemple : Formatage de date internationalisé
import { useIntl, FormattedDate } from 'react-intl';
function MyComponent() {
const intl = useIntl();
const now = new Date();
return (
Date actuelle :
Date actuelle (allemand) :
);
}
export default MyComponent;
Conclusion
La composition d'effets est une technique puissante pour gérer les effets de bord complexes dans les applications React. En décomposant les grands effets en hooks personnalisés plus petits et réutilisables, vous pouvez améliorer la réutilisabilité du code, accroître la lisibilité, simplifier les tests et réduire la complexité globale. Adoptez la composition d'effets pour créer des applications React plus propres, plus maintenables et évolutives pour un public mondial.