Singletons en JavaScript: ¿Herramienta Útil o Trampa Oculta?
Has escrito un módulo que exporta un logger configurado o una instancia de cliente API. Cada archivo lo importa, y asumes que hay exactamente una instancia ejecutándose en toda tu aplicación. Entonces tus pruebas comienzan a filtrar estado entre ejecuciones. O tu arquitectura de microfrontends de repente tiene dos copias de tu “singleton” compitiendo entre sí. ¿Qué sucedió?
El patrón singleton de JavaScript no funciona como sugiere la teoría clásica de patrones de diseño. Entender dónde viven realmente los singletons de módulos ES—y dónde fallan—te ahorra sesiones de depuración que se sienten como perseguir fantasmas.
Puntos Clave
- Los singletons de módulos ES se almacenan en caché por grafo de módulos y runtime, no globalmente en todo tu sistema
- Los singletons funcionan bien para datos inmutables y operaciones sin estado como utilidades de logging y configuración de solo lectura
- El estado mutable en singletons se vuelve peligroso en renderizado del lado del servidor, entornos de prueba y arquitecturas de microfrontends
- Antes de usar un singleton, considera si tu código se ejecuta en múltiples bundles, workers o solicitudes del servidor, y si las pruebas necesitan reiniciar la instancia
Qué Significa Realmente “Singleton” en JavaScript Moderno
Olvida por un momento la definición de Gang of Four. En JavaScript moderno, un singleton es típicamente solo un módulo que exporta un objeto ya instanciado:
// logger.js
class Logger {
log(message) {
console.log(`[${Date.now()}] ${message}`)
}
}
export const logger = new Logger()
Cuando importas logger desde múltiples archivos, obtienes la misma instancia—no por trucos ingeniosos del constructor, sino porque los módulos ES se almacenan en caché por grafo de módulos y runtime. El módulo se ejecuta una vez, la instancia se crea una vez, y cada importación recibe una referencia a ese mismo objeto.
Esta es la base de los singletons de módulos ES. Es simple y a menudo exactamente lo que necesitas.
La Suposición Que Falla
Aquí es donde emergen las trampas de los singletons en JavaScript: esa “instancia única” está limitada a un grafo de módulos específico dentro de un runtime de JavaScript específico. No es mágicamente global en todo tu sistema.
La suposición falla en varios escenarios del mundo real:
Múltiples bundles o paquetes duplicados. Si tu monorepo tiene dos paquetes que cada uno empaqueta su propia copia de una dependencia, obtienes dos grafos de módulos separados. Dos “singletons”. Tu estado compartido ya no está compartido.
Ejecutores de pruebas. Jest, Vitest y herramientas similares a menudo reinician las cachés de módulos entre archivos de prueba o usan procesos worker. Tu singleton de un archivo de prueba puede no ser la misma instancia en otro.
Microfrontends. Cada frontend desplegado independientemente típicamente tiene su propio runtime de JavaScript. Un singleton en un microfrontend es invisible para otro a menos que las instancias se compartan explícitamente a través del build o runtime.
Web Workers y Service Workers. Estos se ejecutan en contextos de JavaScript separados. Un módulo importado en un worker es una instancia completamente diferente del mismo módulo en tu hilo principal.
Runtimes de servidor con aislamiento de solicitudes. En frameworks como Next.js o Nuxt ejecutándose en el servidor, un singleton puede persistir a través de múltiples solicitudes de usuario dependiendo del runtime y modelo de despliegue, arriesgando filtración de datos si mantiene estado mutable específico de la solicitud.
Discover how at OpenReplay.com.
Dónde Funcionan Bien los Singletons
A pesar de estas trampas, los singletons de módulos ES siguen siendo genuinamente útiles para ciertos patrones de utilidad y coordinación en el frontend:
- Utilidades de logging. Un logger compartido con formato consistente no causa daño si se duplica y no mantiene estado sensible por solicitud.
- Snapshots de configuración. La configuración de solo lectura cargada al inicio funciona bien como singleton. La palabra clave es solo lectura.
- Utilidades sin estado. Funciones auxiliares o clases que no mantienen estado mutable entre llamadas son candidatos seguros.
El hilo común: estos singletons mantienen datos inmutables o realizan operaciones sin estado.
Dónde los Singletons Se Convierten en un Problema
El estado mutable es donde las cosas se vuelven peligrosas. Considera:
- Datos de sesión de usuario. En el renderizado del lado del servidor, un singleton que mantiene información de usuario puede filtrarse entre solicitudes.
- Cachés con alcance de solicitud. Datos que deberían reiniciarse por solicitud persisten incorrectamente.
- Configuración mutable compartida. Una parte de tu aplicación modifica configuraciones, afectando inesperadamente a otra.
Las herramientas modernas amplifican estos problemas. En React 18+, el Modo Estricto intencionalmente invoca dos veces los renders y ciertos efectos en desarrollo, exponiendo estado de singleton que no está correctamente aislado. Hot Module Replacement en Vite o webpack puede preservar el estado del singleton a través de cambios de código, creando bugs sutiles donde tu código “fresco” opera sobre datos obsoletos.
Una Postura Práctica
El patrón singleton de JavaScript no es inherentemente malo—simplemente es más limitado de lo que muchos desarrolladores asumen. Antes de recurrir a una instancia a nivel de módulo, pregunta:
- ¿Es este estado verdaderamente inmutable o sin estado?
- ¿Podría este código ejecutarse en múltiples bundles, workers o solicitudes del servidor?
- ¿Necesitarán mis pruebas reiniciar o simular esta instancia?
Si respondes “sí” a las preguntas 2 o 3 con estado mutable, considera alternativas: funciones factory, inyección de dependencias, o patrones específicos del framework como React Context o servicios con alcance de solicitud.
Conclusión
Los singletons son una herramienta útil cuando entiendes su alcance real. Se convierten en una trampa oculta cuando asumes que “instancia única” significa algo que el runtime nunca prometió. Limítate a datos inmutables o sin estado para tus instancias a nivel de módulo, y recurre a inyección de dependencias o patrones factory cuando necesites aislamiento adecuado entre pruebas, solicitudes o límites de runtime.
Preguntas Frecuentes
Jest a menudo reinicia las cachés de módulos entre archivos de prueba o ejecuta pruebas en procesos worker aislados, creando grafos de módulos nuevos y reinstanciando tu singleton. Usa jest.resetModules() o jest.isolateModules() donde sea apropiado, o evita el estado mutable a nivel de módulo inyectando dependencias.
No. Los Web Workers se ejecutan en contextos de JavaScript separados con sus propios grafos de módulos. Un módulo importado en un worker es una instancia completamente diferente del mismo módulo en tu hilo principal. Para compartir estado, debes pasar explícitamente datos entre contextos usando postMessage o SharedArrayBuffer.
Solo para datos inmutables o sin estado. Los singletons en el servidor pueden persistir a través de múltiples solicitudes de usuario dependiendo del runtime y modelo de despliegue, potencialmente filtrando datos sensibles entre usuarios. Para estado específico de solicitud como sesiones de usuario o cachés, usa patrones con alcance de solicitud proporcionados por tu framework en lugar de instancias a nivel de módulo.
Las funciones factory te permiten crear instancias nuevas bajo demanda. La inyección de dependencias pasa instancias explícitamente, facilitando las pruebas. Soluciones específicas del framework como React Context proporcionan gestión de estado con alcance. Para aplicaciones de servidor, los servicios con alcance de solicitud aseguran aislamiento adecuado entre solicitudes mientras mantienen la conveniencia de utilidades compartidas.
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.