How to provide service at the module level?

59 Views Asked by At

I am trying to create a provider for a library module, that when added triggers my factory provider. However, when I do so, the provider never executes the console.log so I know it isn't running...

Here is the example service that I am trying to instantiate:

@Injectable()
export class ExampleService {
  addOptions(...options: any[]) {
    // Set options
  }
}

When the module is created, I would like to create the service and add some default options to it, so I made a factory provider like this, as I don't want to add it at the component level, or for every instance of ExampleService. Just a one time thing for this module.

@NgModule({
  declarations: [ExampleComponent],
  exports: [ExampleComponent],
  providers: [
    {
      provide: ExampleService,
      multi: false,
      useFactory: () => {
        console.log('Creating Example Service...');
        const service = new ExampleService();
        service.addOptions('one', 'two', 'three');
        return service;
      }
    }
  ],
})
export class LibraryModule {}

I also have a component that I want to also inject the service into, but not share the same instance as the one in the module, so I did this:

@Component({
  providers: [ExampleService]
})
export class ExampleComponent {
  constructor(private example: ExampleService) {}
}

When a team in my company uses our library, they would import it like this, and the options would automatically be set from the LibraryModule.

@NgModule({
  imports: [LibraryModule]
})
export class App {}

Currently the one in the component gets created as intended, but the one in the module is not running the factory as I don't see output from the console.log. What Do I need to do to fix this? Or, is this not the correct way to do this?


Note: I am testing this using storybook, not sure if that makes a difference.

Which looks like this:

@Component({
  standalone: true,
  imports: [LibraryModule]
})
export class StorybookExample {
  constructor(private example: ExampleService) {}
}

export default {
  title: 'Test',
  component: StorybookExample,
} as Meta<StorybookExample>;
1

There are 1 best solutions below

3
Shlang On BEST ANSWER

Angular instantiates services when they are used, i.e. when it creates something that has this service injected. That is why just specifying the factory is not enough - the factory is called only when the service is used somewhere.

What you can do is inject the service into the module's constructor:

@NgModule({
  declarations: [ExampleComponent],
  exports: [ExampleComponent],
  providers: [ExampleService],
})
export class LibraryModule {
  constructor(exampleService: ExampleService) {
    service.addOptions('one', 'two', 'three');
  }
}

Please keep in mind that importing this module does not force a new instance creation - if the service was provided and injected somewhere else there could be already an instance that will be passed to the module constructor. That is why the presence of provideIn: 'root' at the service decorator does not make any difference to how many instances are created.

This behavior changes for lazy-loaded modules since they create their injector, so if this module is imported as part of a lazy-loaded module a new instance is created for a scope of the lazy-loaded module.

Playgroud


If this looks messy, it is. Not so long time ago Angular introduced Standalone API, which makes it possible to have an application without using NgModule at all. And there is a special DI token, that can be user for running initialization logic:

  providers: [
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useValue: () => {
        inject(ExampleService).addOptions('one', 'two', 'three');
      }
    }
  ]

This can be specified for bootstrapApplication or for a route configuration.


It may be the case that what is actually needed is a way to pass some configuration to the service. For such cases, it may be better to introduce an InjectionToken that then can be used for providing the configuration

export const ExampleOptions = new InjectionToken<string[]>('Options for Example Service');

@Injectable({
  provideIn: 'root'
})
export class ExampleService {
  options = inject(ExampleOptions);
}

And usage:

// "Global"/"Default" configuration
bootstrapApplication(AppComponent, {
  providers: [{
    provide: ExampleOptions,
    useValue: ['one', 'two', 'three']
  }]
})

// Override for component's scope
@Component({
  /* ... */
  providers: [
    {
      provide: ExampleOptions,
      useValue: ['one', 'two', 'three']
    },
    /* Service should also be provided as Angular is not able to
       understand our intent to create a new instance 
       with a different set of options */
    ExampleService  
  ]
})
class MyComponent() {}

Finally, to make it less verbose, we could create a providers factory, in a similar way to some Angular's API, e.g. provideRouter:

export provideExampleService = (options?: string[]) => [
  options 
    ? { provide: ExampleOptions, useValue: options }
    : [],
  ExampleService
];