
Your Angular Services Might Need Some Privacy

š¤« Private Services
@Component({
providers: [MyStore]
})
class MyCmp {
constructor(private _store: MyStore) {}
}
š„µ The Challenge
providers: [MyStore]
@Component({
providers: [MyStore]
})
class MyCmp {
constructor(@Self() private _store: MyStore) {}
}
NullInjectorError: No provider for MyStore!
@Self()
š The Alternative
// 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
MyStore
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);
}
injectStore()
MyStoreImpl
constructor(@Inject(provideStore()[0]) store: MyStore)
š Boosting The Developer eXperience
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;
}
š Configurable Private Services
š Configurable Private Store
RxState
ComponentStore
// 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});
}
}
ā»ļø Hooking Reactive Store to Change Detection
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
Proxy
RxState
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;
}
}
ngOnDestroy()
ā
About Testing
ComponentStore
RxState
// 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
inject()
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});
}
}