Services e injeção de dependência no Angular

Para entender melhor o conceito de injeção de dependência, comecemos com uma analogia: imagine que você é um chef de cozinha e está trabalhando em um restaurante novo, onde não conhece a localização dos utensílios e ingredientes. No entanto, você conta com a ajuda de um assistente que sabe onde tudo está. Para preparar o prato, você precisa de várias dependências como ingredientes e utensílios, por exemplo farinha, ovos e panelas. Basta solicitar ao assistente, e ele fornecerá tudo o que você precisa. Ele é quem conhece e gerencia a localização de todos os itens necessários.

Trazendo essa analogia para o mundo da programação, o assistente do exemplo acima atua como um injetor de dependências. O principio básico, é que o injetor primeiro tenha conhecimento sobre as dependências registradas nele e então passe a fornecer os recursos solicitados.

Introdução à injeção de dependências no Angular

A Injeção de Dependências (DI) é um recurso fundamental do Angular que facilita o gerenciamento de dependências entre classes na sua aplicação. Ela promove um código mais modular, reutilizável e testável, além de auxiliar na organização e na escalabilidade do seu código.

O que são dependências?

No contexto do Angular, as dependências geralmente são classes com responsabilidades específicas que podem ser reutilizadas em diferentes partes da aplicação. Essas classes podem encapsular lógica de negócios, serviços de dados e outros elementos que fornecem funcionalidades essenciais para a sua aplicação.

Registrando dependências com @Injectable()

O decorator @Injectable() é um das formas para registrar classes como dependências no Angular. Ao aplicar esse decorator a uma classe, você informa ao framework que ela pode ser injetada em outras classes da sua aplicação. O decorator também permite definir o escopo da dependência, que determina a forma como a classe é instanciada e compartilhada.

💡
Para quem trabalha com Java Spring Boot, o @Injectable() é similar ao @Service

O Angular oferece três escopos principais para dependências:

  • Raiz (root): A dependência é registrada no nível da aplicação, tornando-a acessível a qualquer componente. Uma única instância da classe é criada e compartilhada por toda a aplicação.
  • Componente: A dependência é registrada no nível do componente, tornando-a acessível apenas ao componente que a registrou e seus componentes filhos. Uma nova instância da classe é criada para cada componente que a utiliza.
  • Módulo: A dependência é registrada no nível do módulo, tornando-a acessível a todos os componentes dentro do módulo. Uma nova instância da classe é criada para cada instância do módulo.

Registrando uma dependência no nível de aplicação (root)

Neste exemplo, a classe UserService é registrada como uma dependência de nível de aplicação. Isso significa que qualquer componente na sua aplicação pode injetar essa classe e utilizar seus serviços.

Para injetar uma dependência em um componente, utilize o construtor do componente e especifique o tipo da dependência como parâmetro. O Angular automaticamente fornecerá uma instância da dependência para o componente.

// Service
@Injectable({ providedIn: 'root' })
export class UserService {
  // ...
}

// Componente:
@Component({ /* ... */ })
export class LoginComponent {
  // Injeta uma instância do UserService:
  constructor(userService: UserService) {
    // ...
  }
}

Como a injeção de dependência funciona

O registro e a resolução de dependências no Angular segue uma lógica hierárquica. Por exemplo, quando um componente solicita uma dependência, o Angular busca por ele na seguinte ordem:

  1. Busca por dependências registradas no próprio componente.
    Obs.: mais a frente há exemplos mostrando como registrar dependências em um componente utilizando o array providers e viewProviders.
  2. Caso não encontre a dependência, ele busca nos componentes ancestrais.
  3. Se a dependência ainda não for encontrada, busca no EnvironmentInjector que é onde ficam as dependências a nível de aplicação
  4. Por fim, se a dependência não for encontrada em nenhum injetor, o NullInjector é acionado indicando que a dependência não foi encontrada. Nestes casos você pode obter um erro similar a este:

    NullInjectorError: R3InjectorError(Standalone[_LoginComponent])[UserService -> UserService -> UserService]: NullInjectorError: No provider for UserService!

Angular has two injector hierarchies:

EnvironmentInjectorhierarchy: Configure an ElementInjector in this hierarchy using @Injectable() or providers array in ApplicationConfig

ElementInjector
hierarchy: Created implicitly at each DOM element. An ElementInjector is empty by default unless you configure it in the providers property on @Directive() or @Component().

fonte: https://angular.dev/guide/di/hierarchical-dependency-injection

Para facilitar o entendimento, o diagrama abaixo ilustra o processo de resolução de três dependências no componente PasswordRecoveryComponent, mostrando a ordem de busca em cada flecha.

Diagrama ilustrando a resolução de dependências no Angular

Registrando dependências a nível de aplicação (root)

Existem algumas formas de registrar dependências no nível da aplicação (root):

  1. Usando o decorator @Injectable({ providedIn: 'root' })
  2. Declarando os recursos no array providers do ApplicationConfig ou AppModule para aplicações baseadas em módulos.

No exemplo abaixo, mostro várias formas de registrar uma dependência ou valor no nível da aplicação e como utilizá-los em um componente. Deixei alguns comentários ao longo do código para facilitar o entendimento.

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class Root1Service {
  get message() {
    return 'Sou Root1Service';
  }
}

root1.service.ts

// Essa classe será registrada através
// de uma declaração no array providers
// do appConfig (abaixo)
export class Root2Service {
  get message() {
    return 'Sou Root2Service';
  }
}

root2.service.ts

// (imports...)

export const appConfig: ApplicationConfig = {
  providers: [
    //
    // Root1Service já foi registrada utilizando
    // @Injectable({ providedIn: 'root' })
    //

    // Registrando Root2Service
    Root2Service,

    // Registrando um valor qualquer
    {
      provide: 'VALOR_REGISTRADO_NO_ROOT',
      useValue: 'valor registrado no root',
    },

    // Registrando várias classes para um mesmo
    // token (provide). Quem solicitar a
    // dependência LOGGER, irá obter um array.
    //
    // Isso acontece porque estamos utilizando
    // o multi: true.
    //
    // O `multi` é útil, por exemplo, no registro
    // de interceptors.
    //
    // Exemplo no uso de interceptors:
    // (https://consolelog.com.br/angular-interceptor/)
    {
      provide: 'LOGGER',
      useClass: Logger1Service,
      multi: true,
    },
    {
      provide: 'LOGGER',
      useClass: Logger2Service,
      multi: true,
    },

    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
  ],
};

app.config.ts

A seguir foi criado um componente e declaradas suas dependências no construtor. Ao fazer isso, o Angular fornece automaticamente os objetos/valores necessários, pois já indicamos ao framework quais classes e valores serão compartilhados como dependências.

// (imports...)

@Component({
  selector: 'app-exemplo1',
  standalone: true,
  template: ` <p>exemplo1 works!</p> `,
})
export class Exemplo1Component {
  constructor(
    root1Service: Root1Service,
    root2Service: Root2Service,
    @Inject('LOGGER') private loggers: Logger[],
    @Inject('VALOR_REGISTRADO_NO_ROOT')
    valorRegistradoNoRoot: string
  ) {
    this.loggers.forEach((logger) => {
      logger.log(root1Service.message);
      logger.log(root2Service.message);
      logger.log(valorRegistradoNoRoot);
    });
  }
}

exemplo1.component.ts

Ao executar o código acima, o console do DevTools no navegador exibirá as mensagens registradas pelas duas classes de log, Logger1Service e Logger2Service, confirmando que as dependências foram injetadas com sucesso.

Logger1Service:: – "Sou Root1Service"
Logger1Service:: – "Sou Root2Service"
Logger1Service:: – "valor registrado no root"

Logger2Service:: – "Sou Root1Service"
Logger2Service:: – "Sou Root2Service"
Logger2Service:: – "valor registrado no root"

Todas as dependências estavam registradas a nível de aplicação, portanto, qualquer outro componente dessa aplicação teria acesso exatamente as mesmas instâncias de classes que o Exemplo1Component.

Essa abordagem garante que haja apenas uma instância da dependência para toda a aplicação, implementando o padrão singleton. Esse padrão é útil para compartilhar Services entre componentes, pois todos os componentes da aplicação recebem exatamente a mesma instância da Service.

💡
Por aqui já comentamos sobre o uso de services no compartilhamento de dados entre componentes: Compartilhando dados entre componentes Angular com Subject e BehaviorSubject

Um ponto importante para este cenário envolvendo o escopo root, é que para otimizar o build com tree-shaking, a documentação oficial recomenda usar o @Injectable com o parâmetro providedIn. Isso garante que classes registradas no injetor sejam descartadas se não forem utilizadas por nenhum componente.


Using providedIn enables Angular and JavaScript code optimizers to effectively remove services that are unused (known as tree-shaking).

fonte:
https://angular.dev/guide/di/dependency-injection#preferred-at-the-application-root-level-using-providedin

Registrando dependência a nível de componente

A injeção de dependência (DI) a nível de componente no Angular permite registrar serviços e/ou valores que são específicos para um componente e seus filhos. Isso facilita o acesso à dependência por parte do componente e seus descendentes, sem a necessidade de propagar a dependência por toda a hierarquia da aplicação. No entanto, é importante notar que cada vez que o componente é criado, uma nova instância do serviço também é criada.

No exemplo abaixo há dois componentes que possuem uma relação de "pai" e "filho". Observe os seguintes pontos:

  • Ambos conseguem resolver dependências registradas a nível de aplicação, por exemplo, a dependências VALOR_REGISTRADO_NO_ROOT, Root1Service e Root2Service
  • O componente "pai", Exemplo2Component, declara duas dependências. Nesse caso, tanto ele quanto o componente "filho" têm acesso às dependências. No entanto, outros componentes fora dessa hierarquia não têm acesso a essas dependências:
    • Exemplo2Service
    • VALOR_EXEMPLO2_COMPONENT
export class Exemplo2Service {
  get message() {
    return 'Sou Exemplo2Service';
  }
}

exemplo2.service.ts

// (imports...)

@Component({
  selector: 'app-exemplo2',
  standalone: true,
  imports: [FilhoExemplo2Component],
  template: `
    <p>exemplo2 works!</p>
    <app-filho-exemplo2></app-filho-exemplo2>
  `,
  providers: [
    Exemplo2Service,
    {
      provide: 'VALOR_EXEMPLO2_COMPONENT',
      useValue: 'Valor do Exemplo2Component',
    },
  ],
})
export class Exemplo2Component {
  constructor(
    // Dependências registras a nível de aplicação
    root1Service: Root1Service,
    root2Service: Root2Service,
    @Inject('VALOR_REGISTRADO_NO_ROOT')
    valorRegistradoNoRoot: string,

    // Dependências registradas a nível de componente
    exemplo2Service: Exemplo2Service,
    @Inject('VALOR_EXEMPLO2_COMPONENT')
    valorExemplo2Component: string
  ) {
    console.group('Exemplo2Component');
    console.log(root1Service.message);
    console.log(root2Service.message);
    console.log(valorRegistradoNoRoot);
    console.log(exemplo2Service.message);
    console.log(valorExemplo2Component);
    console.groupEnd();
  }
}

exemplo2.component.ts

// (imports...)

@Component({
  selector: 'app-filho-exemplo2',
  standalone: true,
  template: ` <p>filho-exemplo2 works!</p> `,
})
export class FilhoExemplo2Component {
  constructor(
    // Dependências registras a nível de aplicação
    root1Service: Root1Service,
    root2Service: Root2Service,
    @Inject('VALOR_REGISTRADO_NO_ROOT')
    valorRegistradoNoRoot: string,

    // Dependências registradas a nível de componente
    exemplo2Service: Exemplo2Service,
    @Inject('VALOR_EXEMPLO2_COMPONENT')
    valorExemplo2Component: string
  ) {
    console.group('FilhoExemplo2Component');
    console.log(root1Service.message);
    console.log(root2Service.message);
    console.log(valorRegistradoNoRoot);
    console.log(exemplo2Service.message);
    console.log(valorExemplo2Component);
    console.groupEnd();
  }
}

filho-exemplo2.component.ts

Executando o código acima podemos observar as seguintes mensagens no console do DevTools do navegador:

Exemplo2Component
  Sou Root1Service
  Sou Root2Service
  valor registrado no root
  Sou Exemplo2Service
  Valor do Exemplo2Component

FilhoExemplo2Component
  Sou Root1Service
  Sou Root2Service
  valor registrado no root
  Sou Exemplo2Service
  Valor do Exemplo2Component

providers: useValue, useClass e deps

Até este ponto foi apresentado como registrar classes e valores utilizando o decorator @Injectable() e o array providers, tanto a nível de componente quanto aplicação. Agora vamos explorar um pouco mais sobre este recurso.

Quando simplesmente declaramos uma classe no array providers, como abaixo, o Angular por padrão cria uma instância da classe utilizando o new:

providers: [AlgumaService]

// Quando alguém solicitar uma
// instância de AlgumaService, por
// padrão o Angular fará algo do tipo:
// return new AlgumaService();

Porém, existem outras opções para registrarmos nossas dependências, como o useValue, useClasse e o deps. A seguir há um código mostrando um exemplo de cada:

import { HttpClient } from '@angular/common/http';
import {
  Component,
  Inject,
  InjectionToken,
} from '@angular/core';
import { Observable, of } from 'rxjs';

// Criando um token para ser declarado
// no provide: TOKEN
export const USER_REPOSITORY =
  new InjectionToken<IUserRepository>(
    'Indica qual a classe que será utilizada como repositório de usuários'
  );

export interface IUserRepository {
  getUsers(): Observable<string[]>;
}

export class MockUserRepository implements IUserRepository {
  getUsers(): Observable<string[]> {
    return of(['User1', 'User2', 'User3']);
  }
}

// Observe que a classe abaixo depende
// do HttpClient.
export class RemoteUserRepository
  implements IUserRepository
{
  constructor(private httpClient: HttpClient) {}

  getUsers(): Observable<string[]> {
    const url =
      'https://run.mocky.io/v3/0d965afa-03e0-4342-9242-7f374fd500ab';
    return this.httpClient.get<string[]>(url);
  }
}

@Component({
  selector: 'app-exemplo4',
  standalone: true,
  imports: [],
  template: ` <p>exemplo4 works!</p> `,
  providers: [
    // Podemos substituir qual classe será injetada
    // através do useClass
    //
    // Isto é útil por exemplo, nos testes unitários.
    // Nos testes podemos sobrescrever o `provide`
    // registrando uma classe totalmente mockada no
    // `useClass`
    {
      provide: USER_REPOSITORY,
      useClass: MockUserRepository,
    },
    // ou
    {
      provide: USER_REPOSITORY,
      useClass: RemoteUserRepository,
      // Observe que essa classe depende
      // do HttpClient
      deps: [HttpClient],
    },
    // ou
    {
      provide: USER_REPOSITORY,
      // Ao invés de informarmos uma classe,
      // podemos informar um valor diretamente
      useValue: {
        getUsers: () => {
          return of(['User1', 'User2', 'User3']);
        },
      } as IUserRepository,
    },
  ],
})
export class Exemplo4Component {
  constructor(
    @Inject(USER_REPOSITORY)
    userRepository: IUserRepository
  ) {
    userRepository
      .getUsers()
      .subscribe((a) => console.log(a));
  }
}
💡
No Angular 18 o HttpClientModule foi marcado como deprecated. No lugar use o provideHttpClient() dentro do array providers do app.config.ts

providers vs viewProviders

Além do array providers, existe o array viewProviders, que atua de uma forma muito parecida, com a diferença do escopo de atuação.

Todas as dependências declaradas no array providers, podem ser consumidas por componentes filhos. Estes componentes filhos podem estar declarados no template do componente hospedeiro (host) ou serem projetados. Independente da relação, as dependências estarão disponíveis para os componentes filhos.

Já as dependências declaradas no array viewProviders, direcionam seu foco para os componentes filhos diretos, excluindo o conteúdo projetado. Isso significa que apenas os componentes filhos que residem diretamente na estrutura do template do componente hospedeiro terão acesso às dependências declaradas com viewProviders.

Veja no exemplo a seguir um componente pai (hospedeiro/host) que tem um componente filho declarado diretamente no seu template e um conteúdo que será projetado através do <ng-content>. Deixei alguns comentários no código para facilitar o entendimento.

// (imports)

@Component({
  selector: 'app-host',
  standalone: true,
  imports: [FilhoHostComponent],
  providers: [
    {
      provide: 'VALOR_NO_PROVIDERS',
      useValue: 'Valor no providers',
    },
    {
      provide: 'VALOR_DECLARADO_EM_AMBOS',
      useValue: 'Valor declarado em ambos (providers)',
    },
  ],
  viewProviders: [
    {
      provide: 'VALOR_NO_VIEW_PROVIDERS',
      useValue: 'Valor no view providers',
    },
    {
      provide: 'VALOR_DECLARADO_EM_AMBOS',
      useValue: 'Valor declarado em ambos (viewProviders)',
    },
  ],
  template: `
    <!-- Componente filho, declarado no template -->
    <app-filho-host
      loggerName="Filho direto"></app-filho-host>

    <!-- 
      Conteúdo que será projetado. Neste
      exemplo o componente FilhoHostComponent
      será projetado.
    -->
    <ng-content></ng-content>
  `,
})
export class HostComponent {}

host.component.ts

// (imports)

@Component({
  selector: 'app-filho-host',
  standalone: true,
  template: ` <p>filho-host works!</p> `,
})
export class FilhoHostComponent {
  // Mais detalhes sobre o HostAttributeToken:
  // https://consolelog.com.br/frontend-angular-17-3-api-output-host-attribute-token/
  loggerName: string | null = inject(
    new HostAttributeToken('loggerName'),
    { optional: false }
  );

  constructor(
    @Inject('VALOR_NO_PROVIDERS')
    @Optional()
    valorNoProviders?: string,

    @Inject('VALOR_NO_VIEW_PROVIDERS')
    @Optional()
    valorNoViewProviders?: string,

    @Inject('VALOR_DECLARADO_EM_AMBOS')
    @Optional()
    valorDeclaradoEmAmbos?: string
  ) {
    console.group(this.loggerName);
    console.log(valorNoProviders);
    console.log(valorNoViewProviders);
    console.log(valorDeclaradoEmAmbos);
    console.groupEnd();
  }
}

Em um outro componente declarei o seguinte:

<app-host>
    <!-- Aqui estou projetando o 
         app-filho-host no ng-content
         do HostComponent.

         Não se esqueça que dentro do
         template do HostComponente
         existe uma declaração do
         <app-filho-host>
    -->
    <app-filho-host
      loggerName="Filho projetado">
    </app-filho-host>
</app-host>

Resultado:

Filho projetado
  Valor no providers
  null
  Valor declarado em ambos (providers)

Filho direto
  Valor no providers
  Valor no view providers
  Valor declarado em ambos (viewProviders)

Do ponto de vista hierárquico, podemos dizer que o HostComponent possui dois filhos, ou duas instâncias de FilhoHostComponent. Um desses filhos é declarado diretamente no template, o que significa que ele terá acesso aos valores dos arrays providers e viewProviders. Neste caso, o Angular tenta resolver a dependência primeiro em viewProviders e depois em providers.

No outro caso, onde o componente filho é projetado no HostComponent, ele terá acesso apenas aos valores declarados no array providers.

Particularmente, ainda não precisei usar especificamente o viewProviders, mas acho importante entender esse comportamento para facilitar a compreensão do modificador @Host(), que será abordado a seguir.

Entendendo os modificadores @Optional @Self @SkipSelf e @Host

Aqui entramos em um assunto um pouco mais avançado. Como podemos registrar uma dependência em vários níveis da aplicação ao mesmo tempo, eventualmente será necessário ter um controle maior sobre a resolução de uma dependência. Para isto, podemos utilizar alguns modificadores:

@Optional()

O modificador @Optional do Angular é utilizado para injetar dependências opcionais, ou seja, dependências que podem ou não estar presentes no contexto de injeção. Ao utilizar @Optional, o Angular não lança um erro se a dependência não for encontrada, mas em vez disso, retorna null. Isso permite que você trate a ausência da dependência de forma adequada no seu código.

@Self()

O modificador @Self()  restringe a busca por dependências ao próprio componente que está solicitando o recurso. Isso significa que o Angular não buscará a dependência em componentes ancestrais no escopo de injeção de dependência.

Observação: é recomendável utilizar o @Optional() em conjunto com o @Self() para tratar cenários onde a dependência não é resolvida.

@SkipSelf()

Instrui o Angular a ignorar as dependências registradas no próprio componente e buscar a dependência nos níveis ancestrais na hierarquia de injeção de dependências. Isso significa que o Angular subirá na árvore de componentes até encontrar uma instância da dependência registrada em um componente pai.

Assim como o @Self(), é recomendável utilizar o @Optional() em conjunto para tratar cenários onde a dependência não é resolvida.

Exemplo utilizando @Host() @Self() e @SkipSelf()
No exemplo abaixo o provide VALOR3 foi declarado em dois componentes com valores diferentes:

  • Exemplo3
  • FilhoExemplo3

Observe como os modificadores @Self() e SkipSelf() entregam resultados diferentes para este cenário:

// (imports)

@Component({
  selector: 'app-exemplo3',
  standalone: true,
  imports: [FilhoExemplo3Component],
  providers: [
    Exemplo3Service,
    {
      provide: 'VALOR3',
      useValue: 'Valor3 do Exemplo3Component',
    },
  ],
  template: `
    <p>exemplo3 works!</p>
    <app-filho-exemplo3></app-filho-exemplo3>
  `,
})
export class Exemplo3Component {}
// (imports)

@Component({
  selector: 'app-filho-exemplo3',
  standalone: true,
  template: ` <p>filho-exemplo3 works!</p> `,
  providers: [
    {
      provide: 'VALOR3',
      useValue: 'Valor3 do FilhoExemplo3Component',
    },
  ],
})
export class FilhoExemplo3Component {
  constructor(
    @Self()
    @Optional()
    @Inject('VALOR3')
    valor3Self?: string,

    // Dependências registradas em FilhoExemplo3Component
    @SkipSelf()
    @Optional()
    @Inject('VALOR3')
    valor3Skipself?: string,

    @Optional()
    @Inject('VALOR_QUE_NAO_EXISTE')
    valorQuePodeNaoExistir?: string
  ) {
    console.log(`valor3Self=${valor3Self}`);
    console.log(`valor3Skipself=${valor3Skipself}`);
    console.log(
      `valorQuePodeNaoExistir=${valorQuePodeNaoExistir}`
    );
  }
}

Resultado da resolução das dependências no componente FilhoExemplo3:

valor3Self=Valor3 do FilhoExemplo3Component
valor3Skipself=Valor3 do Exemplo3Component
valorQuePodeNaoExistir=null

@Host()

Enquanto no @Self() a busca por dependências limita-se ao próprio componente onde está aplicado, ou seja, se a dependência não for encontrada localmente, a injeção falha, no @Host() essa busca é expandida além do componente atual, buscando no componente hospedeiro (pai) na hierarquia de componentes. Se a dependência ainda não for encontrada, a busca termina.

Aqui novamente temos dois possíveis relacionamentos entre um componente "pai" e um "filho":

  • Componente declarado no template: O componente filho tem acesso às dependências registradas no viewProviders do componente hospedeiro.
  • Componente projetado: O componente filho tem acesso às dependências declaradas no providers do componente hospedeiro.

Considerações

O texto ficou mais longo do que gostaria, mas o assunto é denso e tentei não perder detalhes importantes.

Por fim, entender a injeção de dependência pode te ajudar a construir uma aplicação muito mais robusta, reduzindo o nível de acoplamento, facilitando a manutenção e a construção de testes unitários.

A seguir deixo alguns links interessantes falando sobre o assunto: