Marmicode
Blog Post
Younes Jaaidi

Your Angular Services Might Need Some Privacy

by Younes Jaaidi ā€¢ 
Jun 7, 2022 ā€¢ 9 minutes
Your Angular Services Might Need Some Privacy

A few days ago, I had a new discussion with Alex Rickabaugh (Angular Team) and he just hit me in the brain with a brilliant pattern for solving two common issues with Angular's Dependency Injection. Now, I can't find any other goal in my life except to share this with you šŸ¤—

šŸ¤” BTW, should I rename this blog to alexideas.dev?

šŸ¤« Private Services

As discussed in my previous post about inject() and Injection Functions, there are common use cases where you want to provide a private instance of a service for your declarable; most common ones are local state services like RxAngular's State and NgRx's ComponentStore or the Model-View-Presenter pattern as described by Lars.

In order to provide a local instance of a service for our declarable, we have to add it to the declarable's providers then inject it like this:

@Component({ providers: [MyStore] }) class MyCmp { constructor(private _store: MyStore) {} }

šŸ„µ The Challenge

This approach is error-prone as children might mistakenly end up using the parent's private instance if they omit the providers: [MyStore] line.

That's the reason why it is usually recommended, for these types of services, to use the @Self() decorator in order to make sure we are not using the parent's instance (Cf. Lars' Post):

@Component({ providers: [MyStore] }) class MyCmp { constructor(@Self() private _store: MyStore) {} }

In fact, if we omit the providers: [MyStore] line, instead of using the parent instance, this will throw the following error: NullInjectorError: No provider for MyStore!.

This is quite cool but the @Self() decorator is not very intuitive. It can quickly be omitted and in addition to this, what is the future of constructor parameters' decorators? As a matter of fact, these are one kind of decorators that might never land in ECMAScript.

šŸŒˆ The Alternative

Here we are! Thanks to the new availability of the inject() function in the constructor, we can expose two functions, one to provide the service and one to force the service to be injected with the Self flag. This means that it will not try to inject the service from parent injectors, and we'll be sure to get our own private instance:

// store.ts export function provideStore() { return [MyStore]; } export function injectStore() { return inject(MyStore, InjectFlags.Self); } // my.component.ts @Component({ providers: [provideStore()] }) class MyCmp { store = injectStore(); }

šŸ¦„ The Safer Alternative

In order to make this even safer, we can prevent users from providing the MyStore service manually by never exporting it, or even better, by exporting the interface but never the implementation:

export interface MyStore { ... } // implementation is not exported class MyStoreImpl implements MyStore { ... } export function provideStore() { return [MyStoreImpl]; } export function injectStore(): MyStore { return inject(MyStoreImpl, InjectFlags.Self); }

This way, there is almost no risk that declarables end up using parents' services by mistake. Users will always prefer using the injectStore() function than hacking their way through the MyStoreImpl and injecting it as it would require something as nasty as this: constructor(@Inject(provideStore()[0]) store: MyStore).

šŸš€ Boosting The Developer eXperience

Finally, we can provide a better Developer eXperience through more explicit error messages:

export function injectStore() {
  const store = inject(MyStoreImpl, InjectFlags.Self | InjectFlags.Optional);
  if (store == null) {
    throw new Error(`ā˜¹ļø Oups! It seems that you forgot to provide a private store.
    Try adding "provideStore()" to your declarable's providers.
    Cf. LINK_TO_DOCS_HERE`);
  }
  return store;
}

There is almost no risk that declarables end up using parents' services by mistake and library authors can provide a great Developer eXperience with custom error messages.

šŸ›  Configurable Private Services

Now, here is the idea inspired by the discussion with Alex. As described in my previous post about inject() and Injection Functions, there are use cases where we want to configure our private service. For instance, we might want to:

  • set the initial state and type of RxState or ComponentStore,
  • or customize a reusable Paginator service,
  • or make sure that some state management effects are registered when interacting with some part of the store.

šŸ” Configurable Private Store

With both RxState and ComponentStore, we have to set the type of the private store manually and then initialize it in the constructor. Wouldn't it be nice if we could just set the initial state and let TypeScript inference do the rest?

Well, this is possible!

// store.ts export function describeStore<STATE extends Record<string, unknown>>( initialState: Partial<STATE> ) { @Injectable() class Store extends RxState<STATE> { constructor() { super(); /* Initialize state. */ this.set(initialState); } } return { provideStore() { return [Store]; }, injectStore() { // @todo handle the error as described above. return inject(Store, InjectFlags.Self); }, }; } // my.component.ts const { provideStore, injectStore } = describeStore({counter: 0}); @Component({ providers: [provideStore()] }) class MyCmp { store = injectStore(); reset() { this.store.set({counter: 0}); } }

In this example, the store's type is inferred from the initial state.

ā™»ļø Hooking Reactive Store to Change Detection

As the store is provided in the declarable's injector, we can inject ChangeDetectorRef and mark the declarable for check anytime a change happens (or even using advanced coalescing & priority strategies šŸ˜‰).

export function describeReactiveStore<STATE extends Record<string, unknown>>( initialState: Partial<STATE> ) { @Injectable() class ReactiveStore extends RxState<STATE> { constructor(cdr: ChangeDetectorRef) { super(); /* Initialize state. */ this.set(initialState); /* Schedule change detection on state change. */ this.hold(this.select(), () => cdr.markForCheck()); } } return { provideStore() { ... }, injectStore() { ... }, }; }

šŸ§ø Simplifying the API

Using a Proxy to simplify RxState's API, the component's code can look something like this without any performance drawback (note that we are using OnPush change detection strategy):

const { provide: provideCounterStore, inject: injectCounterStore } = describeReactiveStore({ counter: 0, }); @Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: `<div> <div>{{ store.counter }}</div> <button (click)="increment()">+</button> </div>`, providers: [provideCounterStore()] }) export class CounterReactiveComponent { store = injectCounterStore(); increment() { ++this.store.counter; } }

This approach is a proper alternative to the anti-patterns described in my previous post. In fact, this time, the initialization logic is done in the service or the factory instead of the Injection Function. Also, we don't need any hack to detect the OnDestroy lifecycle hook. As a matter of fact, it works out of the box because the store's ngOnDestroy() method will be called automatically when the component is destroyed.

šŸ’» You can checkout the working demo here: https://stackblitz.com/github/yjaaidi/ng-experiments/tree/private-configurable-store

āœ… About Testing

All the patterns described above are based on hiding the injection token (or the injected service) whether it is statically or dynamically created. This can make testing harder in case we want to test interactions between the declarable and the service or simply provide a test double of the service to our declarable.

In the majority of cases (and without diving into too many details about isolated vs social testing, classism, and mockism), we shouldn't need a test double. Private services like local stores are an implementation detail and we don't want our tests to get coupled to that. In other words, we should be able to switch from ComponentStore to RxState (or vice versa) without changing our tests.

Nevertheless, there will be legitimate cases where we'll want to provide a test double. That's why developers using the patterns above, especially if they are library authors, should supply a way of providing test doubles. This can be achieved by exporting (when using the static approach) or returning (when using the configurable approach) a function dedicated to providing test doubles that can be used to override component providers.

// store.ts export interface Store {...} export function describeStore(...) { @Injectable() class StoreImpl { ... } return { provideStore() { ... }, provideTestingStore( provider: | { useClass: Type<Store> } | { useExisting: Type<Store> } | { useValue: Store } | { useFactory: () => Store } ) { return [ { ...provider, provide: Store, } ]; }, injectStore() { ... }, }; } // my.component.spec.ts TestBed.overrideProviders(MyComponent, { add: [provideTestingStore({useValue: mock})] }); TestBed.createComponent(MyComponent);

šŸ‘‹ Conclusion

Thanks to the availability of the inject() function in declarables' constructors, since Angular 14, the patterns described above become possible. This can and will hopefully improve the Developer eXperience of local stores and similar libraries. This is also an encouraging way of moving reusable logic to configurable services where the configuration could even change the signature of the provided service. We can now imagine things like this:

const { provideFetcher, injectFetcher } = describeFetcher<Recipe, {keywords: string}>({
  paginationMode: 'cursor',
  limit: 10
});

@Component({
  providers: [provideFetcher()],
  template: `
    <mc-items [items]="fetcher.items$ | async"></mc-items> 
    <button (click)="fetcher.next()">NEXT</button>
  `
})
class MyCmp {
  fetcher = injectFetcher();

  constructor() {
    fetcher.setOffset(0); // TypeScript error because method is not present if paginationMode is 'cursor'.
  }

  setKeywords(keywords: string) {
    fetcher.setParams({keywords});
  }
}

šŸ™ Special thanks to Alex for the discussions, the inspiration, and the review of htis post.

šŸ’» Demo of the private configurable store https://stackblitz.com/github/yjaaidi/ng-experiments/tree/private-configurable-store

šŸ’¬ Let's discuss this on github: https://github.com/marmicode/marmicode/discussions/5.