¿POR QUÉ no estás usando estos providers de Angular?

marianocodes

Mariano Álvarez 🇨🇷

Posted on June 4, 2024

¿POR QUÉ no estás usando estos providers de Angular?

En este blog post, vamos a explorar 4 características que no se utilizan comúnmente en Angular, pero que ofrecen un gran poder y flexibilidad para escribir código escalable.

Ponte en al situación que necesitas implementar una lógica para manejar los logs en tu aplicación. Inicialmente, quieres configurar el tipo de log a mostrar: informativo, de error o de depuración. Para lograr esto, crearemos:

Tokens de Inyección (Injection Tokens)

Son una forma simple de inyectar valores en servicios, componentes y directivas utilizando un token como identificador y almacenando un valor.

export type LoggerConfig = 'debug' | 'info' | 'report';

export const LOGGER_CONFIG = new InjectionToken<LoggerConfig>('LOGGER_CONFIG');

export interface Logger {
  log: (message: string) => void;
}
Enter fullscreen mode Exit fullscreen mode

En el bootstrapApplication, vamos a definir su valor utilizando useValue.

useValue

Como su nombre indica, useValue le asigna un valor a un token.

bootstrapApplication(AppComponent, {
  providers: [
    { provide: LOGGER_CONFIG, useValue: 'debug' }
  ]
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Con el token y su valor definido, lo usaremos inyectándolo en un nuevo servicio que se encargará de "loguear" los mensajes.

@Injectable({ providedIn: 'root' })
export class LoggerService  {

constructor(@Inject(LOGGER_CONFIG) private config: LoggerConfig, private http: HttpClient) {}

  log(message: string): void {
    if (this.config === 'debug') { } // ....
    if (this.config === 'info') { } // ....
    if (this.config === 'report') {
      // petición HTTP para almacenar los errores
    }    
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Uno de los problemas es que la opción report hace una llamada al servidor para almacenar los logs, pero esto no tiene sentido hacerlo mientras nos encontramos en desarrollo local.

Para solucionar este problema, crearemos otro servicio que solo se utilizará en producción, con la lógica necesaria para la función log, este caso el supuesto llamado HTTP.


@Injectable({ providedIn: 'root' })
export class ProdLoggerService implements Logger {
  private readonly URL = 'https://production...';

  constructor(
    @Inject(LOGGER_CONFIG) private config: LoggerConfig,
    private http: HttpClient
  ) {}

  log(message: string): void {
    // ...
  }
Enter fullscreen mode Exit fullscreen mode

Ahora, vamos a configurar el servicio para que sea utilizado según el enterno.

useClass

Nos permite reemplazar la implementación de un proveedor con una clase que tenga las mismas propiedades y funciones. En este caso, agregaremos una condición para que solo se use en producción.


bootstrapApplication(AppComponent, {
  providers: [
     ...
    {
      provide: LoggerService,
      useClass: environment.production ? ProdLoggerService : LoggerService,
    },
     ...
  ]
});
Enter fullscreen mode Exit fullscreen mode

Aunque parece magia, no es necesario que hagamos ningún cambio en los components que consume LoggerService, todo sigue funcionando igual

@Component({
  selector: 'app-root',
  template: '<h1>Tokens de Inyección de Dependencias</h1>'
})
export class AppComponent implements OnInit {
  constructor(@Inject(LOGGER_SERVICE) private loggerService: LoggerService) {}

  ngOnInit(): void {
    this.loggerService.log('La aplicación ha iniciado');
  }
}
Enter fullscreen mode Exit fullscreen mode

Aunque hay algo que me molesta, ambos servicios se injecta HttpClient, pero solo uno lo usa. Un servicio solo debería depender de lo que necesita.

useFactory

Vamos a utilizar useFactory para proporcionar solo las dependencias que cada servicio necesita.

function createLogger(
  config: LoggerConfig,
  httpClient: HttpClient
): LoggerService | ProdLoggerService {
  if (environment.production) {
    return new ProdLoggerService(config, httpClient);
  }

  return new LoggerService(config);
}
...

providers: [
  {
    provide: LoggerService,
    useFactory: createLogger,
    deps: [LOGGER_CONFIG, HttpClient],
  },
]
Enter fullscreen mode Exit fullscreen mode

💡 useFactory puedes utilizarlo para otras cosas, como por ejemplo realizar llamadas HTTP y obtener configuraciones que necesita el servicio.

Por último, si un caso eventual, LoggerService tiene muchas métodos y no quieres exponer todos pero si mantener la misma lógica puedes utilizar useExisting.

useExisting

Supongamos que hemos añadido una función a LoggerService llamada delete, pero no queremos que sea accesible desde todos los componentes. Podemos hacer lo siguiente:

export abstract class LoggerUseExistingService {
  abstract log: void;
}

...

providers: [
  {
    provide: LoggerUseExistingService,
    useExisting: LoggerService,
  },
]
Enter fullscreen mode Exit fullscreen mode

Si inyectas LoggerUseExistingService en un componente, la función delete no estará disponible.

Gracias a este tutorial, ya puedes entender algunas practicas comunes en Angular como definir Interceptors o el Locale ID

Interceptor

    {
      provide: HTTP_INTERCEPTORS,
      useFactory: MyInterceptorFactory,
      deps: [
        { provide: 'API_TOKEN', useValue: 'YOUR_API_TOKEN' }
      ],
      multi: true
    }
Enter fullscreen mode Exit fullscreen mode

LOCALE_ID

{ provide: LOCALE_ID, useValue: 'en-US' }
Enter fullscreen mode Exit fullscreen mode

¡Y ahora también sabes cómo crear soluciones más robustas que te permiten reemplazar cualquier proveedor nativo de Angular!

Puedes encontrar un demo completo acá, solo tienes que eliminar quitar los comentarios según el la característica que quieras probar

No olvides dejar tu 'like' y tus preguntas en los comentarios.

Puedes seguirme en:

Twitter | LinkedIn | @marianocodes

💖 💪 🙅 🚩
marianocodes
Mariano Álvarez 🇨🇷

Posted on June 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related