Singletons em JavaScript: Ferramenta Útil ou Armadilha Oculta?
Você escreveu um módulo que exporta um logger configurado ou uma instância de cliente de API. Cada arquivo o importa, e você assume que há exatamente uma instância em execução em toda a sua aplicação. Então seus testes começam a vazar estado entre execuções. Ou sua arquitetura de microfrontends de repente tem duas cópias do seu “singleton” brigando entre si. O que aconteceu?
O padrão singleton em JavaScript não funciona da maneira que a teoria clássica de padrões de design sugere. Entender onde os singletons de módulos ES realmente vivem—e onde eles quebram—te poupa de sessões de debug que parecem uma caça a fantasmas.
Pontos-Chave
- Singletons de módulos ES são armazenados em cache por grafo de módulos e runtime, não globalmente em todo o seu sistema
- Singletons funcionam bem para dados imutáveis e operações sem estado como utilitários de logging e configuração somente leitura
- Estado mutável em singletons se torna perigoso em renderização do lado do servidor, ambientes de teste e arquiteturas de microfrontends
- Antes de usar um singleton, considere se seu código é executado em múltiplos bundles, workers ou requisições de servidor, e se os testes precisam resetar a instância
O Que “Singleton” Realmente Significa no JavaScript Moderno
Esqueça a definição do Gang of Four por um momento. No JavaScript moderno, um singleton é tipicamente apenas um módulo que exporta um objeto já instanciado:
// logger.js
class Logger {
log(message) {
console.log(`[${Date.now()}] ${message}`)
}
}
export const logger = new Logger()
Quando você importa logger de múltiplos arquivos, você obtém a mesma instância—não por causa de truques inteligentes no construtor, mas porque módulos ES são armazenados em cache por grafo de módulos e runtime. O módulo é executado uma vez, a instância é criada uma vez, e cada importação recebe uma referência para esse mesmo objeto.
Esta é a fundação dos singletons de módulos ES. É simples e frequentemente exatamente o que você precisa.
A Suposição Que Quebra
É aqui que as armadilhas de singletons em JavaScript emergem: aquela “instância única” tem escopo para um grafo de módulos específico dentro de um runtime JavaScript específico. Não é magicamente global em todo o seu sistema.
A suposição quebra em vários cenários do mundo real:
Múltiplos bundles ou pacotes duplicados. Se seu monorepo tem dois pacotes que cada um empacota sua própria cópia de uma dependência, você obtém dois grafos de módulos separados. Dois “singletons”. Seu estado compartilhado não é mais compartilhado.
Test runners. Jest, Vitest e ferramentas similares frequentemente resetam caches de módulos entre arquivos de teste ou usam processos worker. Seu singleton de um arquivo de teste pode não ser a mesma instância em outro.
Microfrontends. Cada frontend implantado independentemente tipicamente tem seu próprio runtime JavaScript. Um singleton em um microfrontend é invisível para outro, a menos que as instâncias sejam explicitamente compartilhadas via build ou runtime.
Web Workers e Service Workers. Estes são executados em contextos JavaScript separados. Um módulo importado em um worker é uma instância completamente diferente do mesmo módulo em sua thread principal.
Runtimes de servidor com isolamento de requisição. Em frameworks como Next.js ou Nuxt executando no servidor, um singleton pode persistir através de múltiplas requisições de usuários dependendo do runtime e modelo de implantação, arriscando vazamento de dados se mantiver estado mutável específico da requisição.
Discover how at OpenReplay.com.
Onde Singletons Funcionam Bem
Apesar dessas armadilhas, singletons de módulos ES permanecem genuinamente úteis para certos padrões de utilitários e coordenação em frontend:
- Utilitários de logging. Um logger compartilhado com formatação consistente não causa danos se duplicado e não mantém estado sensível por requisição.
- Snapshots de configuração. Configuração somente leitura carregada na inicialização funciona bem como singleton. A palavra-chave é somente leitura.
- Utilitários sem estado. Funções auxiliares ou classes que não mantêm estado mutável entre chamadas são candidatos seguros.
O fio condutor comum: esses singletons ou mantêm dados imutáveis ou realizam operações sem estado.
Onde Singletons Se Tornam um Passivo
Estado mutável é onde as coisas ficam perigosas. Considere:
- Dados de sessão de usuário. Na renderização do lado do servidor, um singleton mantendo informações de usuário pode vazar entre requisições.
- Caches com escopo de requisição. Dados que deveriam resetar por requisição persistem incorretamente.
- Configuração mutável compartilhada. Uma parte do seu app modifica configurações, afetando inesperadamente outra.
Ferramentas modernas amplificam esses problemas. No React 18+, o Strict Mode intencionalmente invoca duas vezes renderizações e certos efeitos em desenvolvimento, expondo estado de singleton que não está propriamente isolado. Hot Module Replacement no Vite ou webpack pode preservar estado de singleton através de mudanças de código, criando bugs sutis onde seu código “fresco” opera em dados obsoletos.
Uma Postura Prática
O padrão singleton em JavaScript não é inerentemente ruim—é apenas mais restrito do que muitos desenvolvedores assumem. Antes de recorrer a uma instância em nível de módulo, pergunte:
- Este estado é verdadeiramente imutável ou sem estado?
- Este código poderia ser executado em múltiplos bundles, workers ou requisições de servidor?
- Meus testes precisarão resetar ou simular esta instância?
Se você responder “sim” às perguntas 2 ou 3 com estado mutável, considere alternativas: factory functions, injeção de dependência, ou padrões específicos de frameworks como React Context ou serviços com escopo de requisição.
Conclusão
Singletons são uma ferramenta útil quando você entende seu escopo real. Eles se tornam uma armadilha oculta quando você assume que “instância única” significa algo que o runtime nunca prometeu. Mantenha-se em dados imutáveis ou sem estado para suas instâncias em nível de módulo, e recorra à injeção de dependência ou padrões factory quando precisar de isolamento adequado entre testes, requisições ou limites de runtime.
Perguntas Frequentes
O Jest frequentemente reseta caches de módulos entre arquivos de teste ou executa testes em processos worker isolados, criando grafos de módulos frescos e reinstanciando seu singleton. Use jest.resetModules() ou jest.isolateModules() quando apropriado, ou evite estado mutável em nível de módulo injetando dependências.
Não. Web Workers são executados em contextos JavaScript separados com seus próprios grafos de módulos. Um módulo importado em um worker é uma instância completamente diferente do mesmo módulo em sua thread principal. Para compartilhar estado, você deve explicitamente passar dados entre contextos usando postMessage ou SharedArrayBuffer.
Apenas para dados imutáveis ou sem estado. Singletons no servidor podem persistir através de múltiplas requisições de usuários dependendo do runtime e modelo de implantação, potencialmente vazando dados sensíveis entre usuários. Para estado específico de requisição como sessões de usuário ou caches, use padrões com escopo de requisição fornecidos pelo seu framework ao invés de instâncias em nível de módulo.
Factory functions permitem criar instâncias frescas sob demanda. Injeção de dependência passa instâncias explicitamente, facilitando testes. Soluções específicas de frameworks como React Context fornecem gerenciamento de estado com escopo. Para aplicações de servidor, serviços com escopo de requisição garantem isolamento adequado entre requisições enquanto mantêm a conveniência de utilitários compartilhados.
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.