O Angular oferece duas maneiras de tornar services disponíveis para injeção:
- Provisão automática - Usando
providedInno decorator@Injectableou fornecendo uma factory na configuração doInjectionToken - Provisão manual - Usando o array
providersem components, directives, routes ou configuração da aplicação
No guia anterior, você aprendeu como criar services usando providedIn: 'root', que lida com os casos de uso mais comuns. Este guia explora padrões adicionais para configuração de provider tanto automática quanto manual.
Provisão automática para dependências não-classe
Enquanto o decorator @Injectable com providedIn: 'root' funciona muito bem para services (classes), você pode precisar fornecer outros tipos de valores globalmente - como objetos de configuração, funções ou valores primitivos. O Angular fornece InjectionToken para esse propósito.
O que é um InjectionToken?
Um InjectionToken é um objeto que o sistema de dependency injection do Angular usa para identificar exclusivamente valores para injeção. Pense nele como uma chave especial que permite armazenar e recuperar qualquer tipo de valor no sistema DI do Angular:
import { InjectionToken } from '@angular/core';// Create a token for a string valueexport const API_URL = new InjectionToken<string>('api.url');// Create a token for a functionexport const LOGGER = new InjectionToken<(msg: string) => void>('logger.function');// Create a token for a complex typeexport interface Config { apiUrl: string; timeout: number;}export const CONFIG_TOKEN = new InjectionToken<Config>('app.config');
NOTA: O parâmetro string (por exemplo, 'api.url') é uma descrição puramente para debug — o Angular identifica tokens por sua referência de objeto, não por essa string.
InjectionToken com providedIn: 'root'
Um InjectionToken que tem uma factory resulta em providedIn: 'root' por padrão (mas pode ser sobrescrito via a propriedade providedIn).
// 📁 /app/config.token.tsimport { InjectionToken } from '@angular/core';export interface AppConfig { apiUrl: string; version: string; features: Record<string, boolean>;}// Globally available configuration using providedInexport const APP_CONFIG = new InjectionToken<AppConfig>('app.config', { providedIn: 'root', factory: () => ({ apiUrl: 'https://api.example.com', version: '1.0.0', features: { darkMode: true, analytics: false } })});// No need to add to providers array - available everywhere!@Component({ selector: 'app-header', template: `<h1>Version: {{ config.version }}</h1>`})export class HeaderComponent { config = inject(APP_CONFIG); // Automatically available}
Quando usar InjectionToken com funções factory
InjectionToken com funções factory é ideal quando você não pode usar uma classe mas precisa fornecer dependências globalmente:
// 📁 /app/logger.token.tsimport { InjectionToken, inject } from '@angular/core';import { APP_CONFIG } from './config.token';// Logger function typeexport type LoggerFn = (level: string, message: string) => void;// Global logger function with dependenciesexport const LOGGER_FN = new InjectionToken<LoggerFn>('logger.function', { providedIn: 'root', factory: () => { const config = inject(APP_CONFIG); return (level: string, message: string) => { if (config.features.logging !== false) { console[level](`[${new Date().toISOString()}] ${message}`); } }; }});// 📁 /app/storage.token.ts// Providing browser APIs as tokensexport const LOCAL_STORAGE = new InjectionToken<Storage>('localStorage', { // providedIn: 'root' is configured as the default factory: () => window.localStorage});export const SESSION_STORAGE = new InjectionToken<Storage>('sessionStorage', { providedIn: 'root', factory: () => window.sessionStorage});// 📁 /app/feature-flags.token.ts// Complex configuration with runtime logicexport const FEATURE_FLAGS = new InjectionToken<Map<string, boolean>>('feature.flags', { providedIn: 'root', factory: () => { const flags = new Map<string, boolean>(); // Parse from environment or URL params const urlParams = new URLSearchParams(window.location.search); const enableBeta = urlParams.get('beta') === 'true'; flags.set('betaFeatures', enableBeta); flags.set('darkMode', true); flags.set('newDashboard', false); return flags; }});
Esta abordagem oferece várias vantagens:
- Nenhuma configuração manual de provider necessária - Funciona como
providedIn: 'root'para services - Tree-shakeable - Apenas incluído se realmente usado
- Type-safe - Suporte completo ao TypeScript para valores não-classe
- Pode injetar outras dependências - Funções factory podem usar
inject()para acessar outros services
Entendendo a configuração manual de provider
Quando você precisa de mais controle do que providedIn: 'root' oferece, você pode configurar providers manualmente. A configuração manual através do array providers é útil quando:
- O service não tem
providedIn- Services sem provisão automática devem ser fornecidos manualmente - Você quer uma nova instância - Para criar uma instância separada no nível component/directive ao invés de usar a compartilhada
- Você precisa de configuração em tempo de execução - Quando o comportamento do service depende de valores em runtime
- Você está fornecendo valores não-classe - Objetos de configuração, funções ou valores primitivos
Exemplo: Service sem providedIn
import { Injectable, Component, inject } from '@angular/core';// Service without providedIn@Injectable()export class LocalDataStore { private data: string[] = []; addData(item: string) { this.data.push(item); }}// Component must provide it@Component({ selector: 'app-example', // A provider is required here because the `LocalDataStore` service has no providedIn. providers: [LocalDataStore], template: `...`})export class ExampleComponent { dataStore = inject(LocalDataStore);}
Exemplo: Criando instâncias específicas de component
Services com providedIn: 'root' podem ser sobrescritos no nível do component. Isso vincula a instância do service ao ciclo de vida de um component. Como resultado, quando o component é destruído, o service fornecido também é destruído.
import { Injectable, Component, inject } from '@angular/core';@Injectable({ providedIn: 'root' })export class DataStore { private data: ListItem[] = [];}// This component gets its own instance@Component({ selector: 'app-isolated', // Creates new instance of `DataStore` rather than using the root-provided instance. providers: [DataStore], template: `...`})export class IsolatedComponent { dataStore = inject(DataStore); // Component-specific instance}
Hierarquia de injector no Angular
O sistema de dependency injection do Angular é hierárquico. Quando um component solicita uma dependência, o Angular começa com o injector daquele component e sobe na árvore até encontrar um provider para aquela dependência. Cada component na sua árvore de aplicação pode ter seu próprio injector, e esses injectors formam uma hierarquia que espelha sua árvore de components.
Esta hierarquia habilita:
- Instâncias com escopo: Diferentes partes da sua aplicação podem ter diferentes instâncias do mesmo service
- Sobrescrever comportamento: Components filhos podem sobrescrever providers de components pais
- Eficiência de memória: Services são instanciados apenas onde necessário
No Angular, qualquer elemento com um component ou directive pode fornecer valores para todos os seus descendentes.
No exemplo acima:
SocialApppode fornecer valores paraUserProfileeFriendListFriendListpode fornecer valores para injeção emFriendEntry, mas não pode fornecer valores para injeção emUserProfileporque não faz parte da árvore
Declarando um provider
Pense no sistema de dependency injection do Angular como um hash map ou dicionário. Cada objeto de configuração de provider define um par chave-valor:
- Chave (Identificador do provider): O identificador único que você usa para solicitar uma dependência
- Valor: O que o Angular deve retornar quando aquele token for solicitado
Ao fornecer dependências manualmente, você tipicamente vê esta sintaxe abreviada:
import { Component } from '@angular/core';import { LocalService } from './local-service';@Component({ selector: 'app-example', providers: [LocalService] // Service without providedIn})export class ExampleComponent { }
Isso é na verdade uma forma abreviada para uma configuração de provider mais detalhada:
{ // This is the shorthand version providers: [LocalService], // This is the full version providers: [ { provide: LocalService, useClass: LocalService } ]}
Objeto de configuração do provider
Todo objeto de configuração de provider tem duas partes principais:
- Identificador do provider: A chave única que o Angular usa para obter a dependência (definida via a propriedade
provide) - Valor: A dependência real que você quer que o Angular busque, configurada com diferentes chaves baseadas no tipo desejado:
useClass- Fornece uma classe JavaScriptuseValue- Fornece um valor estáticouseFactory- Fornece uma função factory que retorna o valoruseExisting- Fornece um alias para um provider existente
Identificadores de provider
Identificadores de provider permitem ao sistema de dependency injection (DI) do Angular recuperar uma dependência através de um ID único. Você pode gerar identificadores de provider de duas maneiras:
Nomes de classe
Nomes de classe usam a classe importada diretamente como o identificador:
import { Component } from '@angular/core';import { LocalService } from './local-service';@Component({ selector: 'app-example', providers: [ { provide: LocalService, useClass: LocalService } ]})export class ExampleComponent { /* ... */ }
A classe serve tanto como identificador quanto como implementação, que é por isso que o Angular fornece a forma abreviada providers: [LocalService].
Injection tokens
O Angular fornece uma classe InjectionToken integrada que cria uma referência de objeto única para valores injetáveis ou quando você quer fornecer múltiplas implementações da mesma interface.
// 📁 /app/tokens.tsimport { InjectionToken } from '@angular/core';import { DataService } from './data-service.interface';export const DATA_SERVICE_TOKEN = new InjectionToken<DataService>('DataService');
NOTA: A string 'DataService' é uma descrição usada puramente para propósitos de debug. O Angular identifica o token por sua referência de objeto, não por essa string.
Use o token na sua configuração de provider:
import { Component, inject } from '@angular/core';import { LocalDataService } from './local-data-service';import { DATA_SERVICE_TOKEN } from './tokens';@Component({ selector: 'app-example', providers: [ { provide: DATA_SERVICE_TOKEN, useClass: LocalDataService } ]})export class ExampleComponent { private dataService = inject(DATA_SERVICE_TOKEN);}
Interfaces TypeScript podem ser identificadores para injeção?
Interfaces TypeScript não podem ser usadas para injeção porque elas não existem em tempo de execução:
// ❌ This won't work!interface DataService { getData(): string[];}// Interfaces disappear after TypeScript compilation@Component({ providers: [ { provide: DataService, useClass: LocalDataService } // Error! ]})export class ExampleComponent { private dataService = inject(DataService); // Error!}// ✅ Use InjectionToken insteadexport const DATA_SERVICE_TOKEN = new InjectionToken<DataService>('DataService');@Component({ providers: [ { provide: DATA_SERVICE_TOKEN, useClass: LocalDataService } ]})export class ExampleComponent { private dataService = inject(DATA_SERVICE_TOKEN); // Works!}
O InjectionToken fornece um valor em tempo de execução que o sistema DI do Angular pode usar, enquanto ainda mantém type safety através do parâmetro de tipo genérico do TypeScript.
Tipos de valor de provider
useClass
useClass fornece uma classe JavaScript como dependência. Este é o padrão ao usar a sintaxe abreviada:
// Shorthandproviders: [DataService]// Full syntaxproviders: [ { provide: DataService, useClass: DataService }]// Different implementationproviders: [ { provide: DataService, useClass: MockDataService }]// Conditional implementationproviders: [ { provide: StorageService, useClass: environment.production ? CloudStorageService : LocalStorageService }]
Exemplo prático: Substituição de Logger
Você pode substituir implementações para estender funcionalidade:
import { Injectable, Component, inject } from '@angular/core';// Base logger@Injectable()export class Logger { log(message: string) { console.log(message); }}// Enhanced logger with timestamp@Injectable()export class BetterLogger extends Logger { override log(message: string) { super.log(`[${new Date().toISOString()}] ${message}`); }}// Logger that includes user context@Injectable()export class EvenBetterLogger extends Logger { private userService = inject(UserService); override log(message: string) { const name = this.userService.user.name; super.log(`Message to ${name}: ${message}`); }}// In your component@Component({ selector: 'app-example', providers: [ UserService, // EvenBetterLogger needs this { provide: Logger, useClass: EvenBetterLogger } ]})export class ExampleComponent { private logger = inject(Logger); // Gets EvenBetterLogger instance}
useValue
useValue fornece qualquer tipo de dado JavaScript como um valor estático:
providers: [ { provide: API_URL_TOKEN, useValue: 'https://api.example.com' }, { provide: MAX_RETRIES_TOKEN, useValue: 3 }, { provide: FEATURE_FLAGS_TOKEN, useValue: { darkMode: true, beta: false } }]
IMPORTANTE: Tipos e interfaces TypeScript não podem servir como valores de dependência. Eles existem apenas em tempo de compilação.
Exemplo prático: Configuração da aplicação
Um caso de uso comum para useValue é fornecer configuração da aplicação:
// Define configuration interfaceexport interface AppConfig { apiUrl: string; appTitle: string; features: { darkMode: boolean; analytics: boolean; };}// Create injection tokenexport const APP_CONFIG = new InjectionToken<AppConfig>('app.config');// Define configurationconst appConfig: AppConfig = { apiUrl: 'https://api.example.com', appTitle: 'My Application', features: { darkMode: true, analytics: false }};// Provide in bootstrapbootstrapApplication(AppComponent, { providers: [ { provide: APP_CONFIG, useValue: appConfig } ]});// Use in component@Component({ selector: 'app-header', template: `<h1>{{ title }}</h1>`})export class HeaderComponent { private config = inject(APP_CONFIG); title = this.config.appTitle;}
useFactory
useFactory fornece uma função que gera um novo valor para injeção:
export const loggerFactory = (config: AppConfig) => { return new LoggerService(config.logLevel, config.endpoint);};providers: [ { provide: LoggerService, useFactory: loggerFactory, deps: [APP_CONFIG] // Dependencies for the factory function }]
Você pode marcar dependências de factory como opcionais:
import { Optional } from '@angular/core';providers: [ { provide: MyService, useFactory: (required: RequiredService, optional?: OptionalService) => { return new MyService(required, optional || new DefaultService()); }, deps: [RequiredService, [new Optional(), OptionalService]] }]
Exemplo prático: Cliente API baseado em configuração
Aqui está um exemplo completo mostrando como usar uma factory para criar um service com configuração em runtime:
// Service that needs runtime configurationclass ApiClient { constructor( private http: HttpClient, private baseUrl: string, private rateLimitMs: number ) {} async fetchData(endpoint: string) { // Apply rate limiting based on user tier await this.applyRateLimit(); return this.http.get(`${this.baseUrl}/${endpoint}`); } private async applyRateLimit() { // Simplified example - real implementation would track request timing return new Promise(resolve => setTimeout(resolve, this.rateLimitMs)); }}// Factory function that configures based on user tierimport { inject } from '@angular/core';import { HttpClient } from '@angular/common/http';const apiClientFactory = () => { const http = inject(HttpClient); const userService = inject(UserService); // Assuming userService provides these values const baseUrl = userService.getApiBaseUrl(); const rateLimitMs = userService.getRateLimit(); return new ApiClient(http, baseUrl, rateLimitMs);};// Provider configurationexport const apiClientProvider = { provide: ApiClient, useFactory: apiClientFactory};// Usage in component@Component({ selector: 'app-dashboard', providers: [apiClientProvider]})export class DashboardComponent { private apiClient = inject(ApiClient);}
useExisting
useExisting cria um alias para um provider que já foi definido. Ambos os tokens retornam a mesma instância:
providers: [ NewLogger, // The actual service { provide: OldLogger, useExisting: NewLogger } // The alias]
IMPORTANTE: Não confunda useExisting com useClass. useClass cria instâncias separadas, enquanto useExisting garante que você obtenha a mesma instância singleton.
Múltiplos providers
Use a flag multi: true quando múltiplos providers contribuem valores para o mesmo token:
export const INTERCEPTOR_TOKEN = new InjectionToken<Interceptor[]>('interceptors');providers: [ { provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true }, { provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true }, { provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true }]
Quando você injeta INTERCEPTOR_TOKEN, você receberá um array contendo instâncias de todos os três interceptors.
Onde você pode especificar providers?
O Angular oferece vários níveis onde você pode registrar providers, cada um com diferentes implicações para escopo, ciclo de vida e performance:
- Bootstrap da aplicação - Singletons globais disponíveis em todos os lugares
- Em um elemento (component ou directive) - Instâncias isoladas para árvores de components específicas
- Route - Services específicos de feature para módulos lazy-loaded
Bootstrap da aplicação
Use providers no nível da aplicação em bootstrapApplication quando:
- O service é usado em múltiplas áreas de feature - Services como clientes HTTP, logging ou autenticação que muitas partes da sua aplicação precisam
- Você quer um singleton verdadeiro - Uma instância compartilhada por toda a aplicação
- O service não tem configuração específica de component - Utilitários de propósito geral que funcionam da mesma forma em todos os lugares
- Você está fornecendo configuração global - Endpoints de API, feature flags ou configurações de ambiente
// main.tsbootstrapApplication(AppComponent, { providers: [ { provide: API_BASE_URL, useValue: 'https://api.example.com' }, { provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true }, LoggingService, // Used throughout the app { provide: ErrorHandler, useClass: GlobalErrorHandler } ]});
Benefícios:
- Instância única reduz uso de memória
- Disponível em todos os lugares sem configuração adicional
- Mais fácil gerenciar estado global
Desvantagens:
- Sempre incluído no seu bundle JavaScript, mesmo se o valor nunca for injetado
- Não pode ser facilmente customizado por feature
- Mais difícil testar components individuais isoladamente
Por que fornecer durante o bootstrap ao invés de usar providedIn: 'root'?
Você pode querer um provider durante o bootstrap quando:
- O provider tem efeitos colaterais (por exemplo, instalando o router client-side)
- O provider requer configuração (por exemplo, routes)
- Você está usando o padrão
provideSomethingdo Angular (por exemplo,provideRouter,provideHttpClient)
Providers de component ou directive
Use providers de component ou directive quando:
- O service tem estado específico do component - Validadores de formulário, caches específicos de component ou gerenciadores de estado de UI
- Você precisa de instâncias isoladas - Cada component precisa de sua própria cópia do service
- O service é usado apenas por uma árvore de components - Services especializados que não precisam de acesso global
- Você está criando components reutilizáveis - Components que devem funcionar independentemente com seus próprios services
// Specialized form component with its own validation service@Component({ selector: 'app-advanced-form', providers: [ FormValidationService, // Each form gets its own validator { provide: FORM_CONFIG, useValue: { strictMode: true } } ]})export class AdvancedFormComponent { }// Modal component with isolated state management@Component({ selector: 'app-modal', providers: [ ModalStateService // Each modal manages its own state ]})export class ModalComponent { }
Benefícios:
- Melhor encapsulamento e isolamento
- Mais fácil testar components individualmente
- Múltiplas instâncias podem coexistir com diferentes configurações
Desvantagens:
- Nova instância criada para cada component (maior uso de memória)
- Nenhum estado compartilhado entre components
- Deve ser fornecido onde for necessário
- Sempre incluído no mesmo bundle JavaScript que o component ou directive, mesmo se o valor nunca for injetado
NOTA: Se múltiplas directives no mesmo elemento fornecem o mesmo token, uma vencerá, mas qual delas é indefinido.
Providers de route
Use providers no nível de route para:
- Services específicos de feature - Services necessários apenas para routes particulares ou módulos de feature
- Dependências de módulo lazy-loaded - Services que devem carregar apenas com features específicas
- Configuração específica de route - Configurações que variam por área da aplicação
// routes.tsexport const routes: Routes = [ { path: 'admin', providers: [ AdminService, // Only loaded with admin routes { provide: FEATURE_FLAGS, useValue: { adminMode: true } } ], loadChildren: () => import('./admin/admin.routes') }, { path: 'shop', providers: [ ShoppingCartService, // Isolated shopping state PaymentService ], loadChildren: () => import('./shop/shop.routes') }];
Padrões de autores de biblioteca
Ao criar bibliotecas Angular, você frequentemente precisa fornecer opções de configuração flexíveis para consumidores enquanto mantém APIs limpas. As próprias bibliotecas do Angular demonstram padrões poderosos para alcançar isso.
O padrão provide
Ao invés de exigir que usuários configurem manualmente providers complexos, autores de bibliotecas podem exportar funções que retornam configurações de provider:
// 📁 /libs/analytics/src/providers.tsimport { InjectionToken, Provider, inject } from '@angular/core';// Configuration interfaceexport interface AnalyticsConfig { trackingId: string; enableDebugMode?: boolean; anonymizeIp?: boolean;}// Internal token for configurationconst ANALYTICS_CONFIG = new InjectionToken<AnalyticsConfig>('analytics.config');// Main service that uses the configurationexport class AnalyticsService { private config = inject(ANALYTICS_CONFIG); track(event: string, properties?: any) { // Implementation using config }}// Provider function for consumersexport function provideAnalytics(config: AnalyticsConfig): Provider[] { return [ { provide: ANALYTICS_CONFIG, useValue: config }, AnalyticsService ];}// Usage in consumer app// main.tsbootstrapApplication(AppComponent, { providers: [ provideAnalytics({ trackingId: 'GA-12345', enableDebugMode: !environment.production }) ]});
Padrões avançados de provider com opções
Para cenários mais complexos, você pode combinar múltiplas abordagens de configuração:
// 📁 /libs/http-client/src/provider.tsimport { Provider, InjectionToken, inject } from '@angular/core';// Feature flags for optional functionalityexport enum HttpFeatures { Interceptors = 'interceptors', Caching = 'caching', Retry = 'retry'}// Configuration interfacesexport interface HttpConfig { baseUrl?: string; timeout?: number; headers?: Record<string, string>;}export interface RetryConfig { maxAttempts: number; delayMs: number;}// Internal tokensconst HTTP_CONFIG = new InjectionToken<HttpConfig>('http.config');const RETRY_CONFIG = new InjectionToken<RetryConfig>('retry.config');const HTTP_FEATURES = new InjectionToken<Set<HttpFeatures>>('http.features');// Core serviceclass HttpClientService { private config = inject(HTTP_CONFIG, { optional: true }); private features = inject(HTTP_FEATURES); get(url: string) { // Use config and check features }}// Feature servicesclass RetryInterceptor { private config = inject(RETRY_CONFIG); // Retry logic}class CacheInterceptor { // Caching logic}// Main provider functionexport function provideHttpClient( config?: HttpConfig, ...features: HttpFeature[]): Provider[] { const providers: Provider[] = [ { provide: HTTP_CONFIG, useValue: config || {} }, { provide: HTTP_FEATURES, useValue: new Set(features.map(f => f.kind)) }, HttpClientService ]; // Add feature-specific providers features.forEach(feature => { providers.push(...feature.providers); }); return providers;}// Feature configuration functionsexport interface HttpFeature { kind: HttpFeatures; providers: Provider[];}export function withInterceptors(...interceptors: any[]): HttpFeature { return { kind: HttpFeatures.Interceptors, providers: interceptors.map(interceptor => ({ provide: INTERCEPTOR_TOKEN, useClass: interceptor, multi: true })) };}export function withCaching(): HttpFeature { return { kind: HttpFeatures.Caching, providers: [CacheInterceptor] };}export function withRetry(config: RetryConfig): HttpFeature { return { kind: HttpFeatures.Retry, providers: [ { provide: RETRY_CONFIG, useValue: config }, RetryInterceptor ] };}// Consumer usage with multiple featuresbootstrapApplication(AppComponent, { providers: [ provideHttpClient( { baseUrl: 'https://api.example.com' }, withInterceptors(AuthInterceptor, LoggingInterceptor), withCaching(), withRetry({ maxAttempts: 3, delayMs: 1000 }) ) ]});
Por que usar funções de provider ao invés de configuração direta?
Funções de provider oferecem várias vantagens para autores de bibliotecas:
- Encapsulamento - Tokens internos e detalhes de implementação permanecem privados
- Type safety - TypeScript garante configuração correta em tempo de compilação
- Flexibilidade - Fácil composição de features com o padrão
with* - À prova de futuro - Implementação interna pode mudar sem quebrar consumidores
- Consistência - Alinha com os próprios padrões do Angular (
provideRouter,provideHttpClient, etc.)
Este padrão é extensivamente usado nas próprias bibliotecas do Angular e é considerado uma boa prática para autores de bibliotecas que precisam fornecer services configuráveis.