Les Singletons en JavaScript : Outil Utile ou Piège Caché ?
Vous avez écrit un module qui exporte une instance configurée de logger ou de client API. Chaque fichier l’importe, et vous supposez qu’il existe exactement une seule instance en cours d’exécution dans toute votre application. Puis vos tests commencent à fuir de l’état entre les exécutions. Ou votre architecture de microfrontends se retrouve soudainement avec deux copies de votre « singleton » qui se battent entre elles. Que s’est-il passé ?
Le pattern singleton en JavaScript ne fonctionne pas comme le suggère la théorie classique des design patterns. Comprendre où vivent réellement les singletons des modules ES — et où ils échouent — vous évite des sessions de débogage qui ressemblent à une chasse aux fantômes.
Points Clés à Retenir
- Les singletons de modules ES sont mis en cache par graphe de modules et runtime, et non globalement dans tout votre système
- Les singletons fonctionnent bien pour les données immuables et les opérations sans état comme les utilitaires de logging et la configuration en lecture seule
- L’état mutable dans les singletons devient dangereux dans le rendu côté serveur, les environnements de test et les architectures de microfrontends
- Avant d’utiliser un singleton, demandez-vous si votre code s’exécute dans plusieurs bundles, workers ou requêtes serveur, et si les tests doivent réinitialiser l’instance
Ce Que « Singleton » Signifie Réellement en JavaScript Moderne
Oubliez un instant la définition du Gang of Four. En JavaScript moderne, un singleton est généralement juste un module qui exporte un objet déjà instancié :
// logger.js
class Logger {
log(message) {
console.log(`[${Date.now()}] ${message}`)
}
}
export const logger = new Logger()
Lorsque vous importez logger depuis plusieurs fichiers, vous obtenez la même instance — non pas à cause d’astuces de constructeur ingénieuses, mais parce que les modules ES sont mis en cache par graphe de modules et runtime. Le module s’exécute une fois, l’instance est créée une fois, et chaque import reçoit une référence vers ce même objet.
C’est le fondement des singletons de modules ES. C’est simple et souvent exactement ce dont vous avez besoin.
L’Hypothèse Qui Se Brise
C’est là que les pièges des singletons en JavaScript émergent : cette « instance unique » est limitée à un graphe de modules spécifique au sein d’un runtime JavaScript spécifique. Elle n’est pas magiquement globale dans tout votre système.
L’hypothèse se brise dans plusieurs scénarios du monde réel :
Bundles multiples ou packages dupliqués. Si votre monorepo contient deux packages qui bundlent chacun leur propre copie d’une dépendance, vous obtenez deux graphes de modules séparés. Deux « singletons ». Votre état partagé n’est plus partagé.
Test runners. Jest, Vitest et des outils similaires réinitialisent souvent les caches de modules entre les fichiers de test ou utilisent des processus workers. Votre singleton d’un fichier de test peut ne pas être la même instance dans un autre.
Microfrontends. Chaque frontend déployé indépendamment a généralement son propre runtime JavaScript. Un singleton dans un microfrontend est invisible pour un autre, sauf si les instances sont explicitement partagées via le build ou le runtime.
Web Workers et Service Workers. Ceux-ci s’exécutent dans des contextes JavaScript séparés. Un module importé dans un worker est une instance complètement différente du même module dans votre thread principal.
Runtimes serveur avec isolation des requêtes. Dans des frameworks comme Next.js ou Nuxt s’exécutant côté serveur, un singleton peut persister entre plusieurs requêtes utilisateur selon le runtime et le modèle de déploiement, risquant une fuite de données s’il contient un état mutable spécifique à la requête.
Discover how at OpenReplay.com.
Où les Singletons Fonctionnent Bien
Malgré ces pièges, les singletons de modules ES restent véritablement utiles pour certains patterns d’utilitaires et de coordination frontend :
- Utilitaires de logging. Un logger partagé avec un formatage cohérent ne cause aucun dommage s’il est dupliqué et ne contient pas d’état sensible par requête.
- Snapshots de configuration. Une configuration en lecture seule chargée au démarrage fonctionne bien comme singleton. Le mot clé est lecture seule.
- Utilitaires sans état. Les fonctions ou classes d’aide qui ne maintiennent pas d’état mutable entre les appels sont des candidats sûrs.
Le fil conducteur : ces singletons contiennent soit des données immuables, soit effectuent des opérations sans état.
Où les Singletons Deviennent un Handicap
L’état mutable est là où les choses deviennent dangereuses. Considérez :
- Données de session utilisateur. Dans le rendu côté serveur, un singleton contenant des informations utilisateur peut fuir entre les requêtes.
- Caches limités à la requête. Les données qui devraient se réinitialiser par requête persistent incorrectement.
- Configuration mutable partagée. Une partie de votre application modifie des paramètres, affectant de manière inattendue une autre partie.
L’outillage moderne amplifie ces problèmes. Dans React 18+, le Strict Mode invoque intentionnellement deux fois les rendus et certains effets en développement, exposant l’état singleton qui n’est pas correctement isolé. Le Hot Module Replacement dans Vite ou webpack peut préserver l’état singleton entre les changements de code, créant des bugs subtils où votre code « frais » opère sur des données obsolètes.
Une Position Pragmatique
Le pattern singleton en JavaScript n’est pas intrinsèquement mauvais — il est juste plus restreint que ce que beaucoup de développeurs supposent. Avant d’opter pour une instance au niveau du module, demandez-vous :
- Cet état est-il vraiment immuable ou sans état ?
- Ce code pourrait-il s’exécuter dans plusieurs bundles, workers ou requêtes serveur ?
- Mes tests devront-ils réinitialiser ou mocker cette instance ?
Si vous répondez « oui » aux questions 2 ou 3 avec un état mutable, envisagez des alternatives : fonctions factory, injection de dépendances, ou patterns spécifiques au framework comme React Context ou les services limités à la requête.
Conclusion
Les singletons sont un outil utile lorsque vous comprenez leur portée réelle. Ils deviennent un piège caché lorsque vous supposez que « instance unique » signifie quelque chose que le runtime n’a jamais promis. Tenez-vous-en aux données immuables ou sans état pour vos instances au niveau du module, et optez pour l’injection de dépendances ou les patterns factory lorsque vous avez besoin d’une isolation appropriée entre les tests, les requêtes ou les frontières de runtime.
FAQ
Jest réinitialise souvent les caches de modules entre les fichiers de test ou exécute les tests dans des processus workers isolés, créant de nouveaux graphes de modules et réinstanciant votre singleton. Utilisez jest.resetModules() ou jest.isolateModules() le cas échéant, ou évitez l'état mutable au niveau du module en injectant les dépendances.
Non. Les Web Workers s'exécutent dans des contextes JavaScript séparés avec leurs propres graphes de modules. Un module importé dans un worker est une instance complètement différente du même module dans votre thread principal. Pour partager l'état, vous devez explicitement passer des données entre les contextes en utilisant postMessage ou SharedArrayBuffer.
Uniquement pour les données immuables ou sans état. Les singletons côté serveur peuvent persister entre plusieurs requêtes utilisateur selon le runtime et le modèle de déploiement, risquant potentiellement de fuir des données sensibles entre utilisateurs. Pour l'état spécifique à la requête comme les sessions utilisateur ou les caches, utilisez plutôt les patterns limités à la requête fournis par votre framework au lieu d'instances au niveau du module.
Les fonctions factory permettent de créer de nouvelles instances à la demande. L'injection de dépendances passe les instances explicitement, facilitant les tests. Les solutions spécifiques au framework comme React Context fournissent une gestion d'état limitée. Pour les applications serveur, les services limités à la requête garantissent une isolation appropriée entre les requêtes tout en maintenant la commodité des utilitaires partagés.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.