First release of the signal-based router store. It has similar features to @ngworker/router-component-store but uses Angular Signals instead of RxJS Observables and has a peer dependency on NgRx Signals (@ngrx/signals) instead of NgRx Component Store (@ngrx/component-store).
RouterSignalStoreshared API for dependency injectionprovideLocalRouterSignalStorefor providing a local router signal store, a replacement forActivatedRouteprovideGlobalRouterSignalStorefor providing the global router signal store, a replacement for NgRx Router Store- Strong and strict typing with
StrictQueryParams,StrictRouteData, andStrictRouteParams - Serializable router state with
MinimalActivatedRouteSnapshot
Compatibility
- Require Angular 17.0
- Require
@ngrx/signals17.0 - Require TypeScript 5.2
Compatibility
- Require Angular 17.0
- Require
@ngrx/component-store17.0 - Require TypeScript 5.2
- Add
RouterStore#selectRouteDataParam(param: string)to match NgRx Router Store selector name (#341)
Deprecations
- Deprecate
RouterStore#selectRouteData- Use
RouterStore#selectRouteDataParaminstead - To be removed in version 18
- Use
BREAKING CHANGES
Compatibility
- Require Angular 16.0
- Require
@ngrx/component-store16.0 - Require RxJS 7.5
- Require TypeScript 4.9
First stable release. No functional or API changes from 15.0.0-rc.2.
Provide local or global router store using provideLocalRouterStore or provideGlobalRouterStore, respectively
- A local router store matches the
ActivatedRouteservice's observable properties and follow the lifecycle of the component that provides it - The global router store matches the
@ngrx/router-storeselectors and is never destroyed
Both local and global stores implement a common RouterStore API:
currentRoute$fragment$queryParams$routeData$routeParams$title$url$selectQueryParam(param: string)selectRouteData(key: string)selectRouteParam(param: string)selectRouterEvents(...acceptedRouterEvents: RouterEvent[])
RouterStore is also the injection symbol usable through constructor injection, inject, TestBed.inject, and Injector.get. When RouterStore is injected, it resolves to the closest provided local or global router store according to element and environment injectors.
RouterStore uses a serializable router state called MinimalActivatedRouteSnapshot. It uses additional strict, immutable types like StrictQueryParams, StrictRouteData, and StrictRouteParams.
- Use
StrictQueryParamsfor query parameters instead ofStrictRouteParams(#331)
Array query parameters like ?size=m&size=l&size=xl are now correctly resolved to readonly string[] instead of string.
BREAKING CHANGES
RouterStore#queryParams$ and MinimalActivatedRouteSnapshot#queryParams use StrictQueryParams
RouterStore#queryParams$ and MinimalActivatedRouteSnapshot#queryParams use StrictQueryParams instead of StrictRouteParams. Members are of type string | readonly string[] | undefined instead of string | undefined.
The TypeScript compiler will fail to compile code that does not handle the string array type.
BEFORE:
// shirts.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class ShirtsComponent {
#routerStore = inject(RouterStore);
size$: Observable<string> = this.#routerStore.queryParams$.pipe(map((params) => params['size']));
}AFTER:
// shirts.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class ShirtsComponent {
#routerStore = inject(RouterStore);
size$: Observable<readonly string[]> = this.#routerStore.queryParams$.pipe(
map((params) => params['size']),
map((size) => size ?? []),
map((size) => (Array.isArray(size) ? size : [size]))
);
}RouterStore#selectQueryParam use StrictQueryParams
RouterStore#selectQueryParam use StrictQueryParams instead of StrictRouteParams. The returned value is of type string | readonly string[] | undefined instead of string | undefined.
The TypeScript compiler will fail to compile code that does not handle the string array type.
BEFORE:
// shirts.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class ShirtsComponent {
#routerStore = inject(RouterStore);
size$: Observable<string> = this.#routerStore.selectQueryParam('size');
}AFTER:
// shirts.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class ShirtsComponent {
#routerStore = inject(RouterStore);
size$: Observable<readonly string[]> = this.#routerStore.selectQueryParam('size').pipe(
map((size) => size ?? []),
map((size) => (Array.isArray(size) ? size : [size]))
);
}LocalRouterStorematchesActivatedRoutemore closely (#309)- Use
ActivatedRouteto serialize the router state for the local router store implementation (LocalRouterStore) LocalRouterStore.currentRoute$matchesActivatedRoute.snapshot
- Use
- Remove optional type parameter from
RouterStore#selectRouteData(#316) - Replace
MinimalRouteDatawithStrictRouteData(#319) - Change
RouterStore#routeData$andMinimalActivatedRouteSnapshot#datatypes fromDatatoStrictRouteData(#319) - Use strict and immutable route parameters (#319, #321)
- Use strict and immutable query parameters (#320)
BREAKING CHANGES
LocalRouterStore.currentRoute$ matches ActivatedRoute.snapshot
This change in implementation will make the local router store more closely match ActivatedRoute while the global router store matches NgRx Router Store selectors. Through complex route configurations, the router store implementations are exercised to identify edge case differences between them and any breaking changes introduced to the local router store.
BEFORE:
// URL: /parent/child/grandchild
@Component({
/* (...) */
providers: [provideLocalRouterStore()],
})
export class ChildComponent implements OnInit {
#route = inject(ActivatedRoute);
#routerStore = inject(RouterStore);
ngOnInit() {
const currentRouteSnapshot = this.#route.snapshot;
console.log(currentRouteSnapshot.routeConfig.path);
// -> "child"
console.log(currentRouteSnapshot.url[0].path);
// -> "child"
firstValueFrom(this.#routerStore.currentRoute$).then((currentRoute) => {
console.log(currentRoute.routeConfig.path);
// -> "grandchild"
console.log(currentRoute.url[0].path);
// -> "grandchild"
});
}
}AFTER:
// URL: /parent/child/grandchild
@Component({
/* (...) */
providers: [provideLocalRouterStore()],
})
export class ChildComponent implements OnInit {
#route = inject(ActivatedRoute);
#routerStore = inject(RouterStore);
ngOnInit() {
const currentRouteSnapshot = this.#route.snapshot;
console.log(currentRouteSnapshot.routeConfig.path);
// -> "child"
console.log(currentRouteSnapshot.url[0].path);
// -> "child"
firstValueFrom(this.#routerStore.currentRoute$).then((currentRoute) => {
console.log(currentRoute.routeConfig.path);
// -> "child"
console.log(currentRoute.url[0].path);
// -> "child"
});
}
}The type parameter is removed from RouterStore#selectRouteData for stricter typing and to enforce coercion
BEFORE:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class HeroesComponent {
#routerStore = inject(RouterStore);
limit$ = this.#routerStore.selectRouteData<number>('limit');
}AFTER:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class HeroesComponent {
#routerStore = inject(RouterStore);
limit$ = this.#routerStore.selectRouteData('limit').pipe(x => Number(x));The RouterStore#routeData$ selector emits StrictRouteData instead of Data
BEFORE:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class HeroesComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.routeData$.pipe(map((routeData) => routeData['limit']));
}AFTER:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class HeroesComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.routeData$.pipe(
map(routeData => routeData['limit']),
map(x => Number(x))
);RouterStore#routeParams$ and MinimalActivatedRouteSnapshot#params use StrictRouteData instead of Params. Members are read-only and of type string | undefined instead of any
TypeScript will fail to compile application code that has assumed a route type parameter type other than string | undefined.
BEFORE:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class DashboardComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.routeParams$.pipe(map((params) => params['limit']));
}AFTER:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class DashboardComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.routeParams$.pipe(map((params) => Number(params['limit'] ?? 10)));
}StrictRouteData members are now read-only
TypeScript will fail to compile application code that mutates route data data structures.
BEFORE:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class DashboardComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.routeData$.pipe(
map((data) => {
data['limit'] = Number(data['limit']);
return data;
}),
map((data) => data['limit'])
);
}AFTER:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class DashboardComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.routeData$.pipe(map((data) => Number(data['limit'])));
}RouterStore#queryParams$ and MinimalActivatedRouteSnapshot#queryParams use StrictRouteParams instead of Params. Members are read-only and of type string | undefined instead of any
TypeScript will fail to compile application code that has assumed a query parameter type other than string | undefined.
BEFORE:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class DashboardComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.queryParams$.pipe(map((params) => params['limit']));
}AFTER:
// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
})
export class DashboardComponent {
#routerStore = inject(RouterStore);
limit$: Observable<number> = this.#routerStore.queryParams$.pipe(map((params) => Number(params['limit'] ?? 10)));
}Compatibility
To avoid compatibility issues, we now require the same RxJS peer dependency as NgRx ComponentStore, namely at least RxJS version 7.5 (#311).
- Require Angular 15.0
- Require
@ngrx/component-store15.0 - Require RxJS 7.5
- Require TypeScript 4.8
- Ignore non-essential router events when serializing the router state. Only
NavigationStart,RoutesRecognized,NavigationEnd,NavigationCancel, andNavigationErrorevents are essential.
- Add factory for selecting router events of specific types:
RouterStore#selectRouterEvents
- Add factory for selecting specific route data:
RouterStore#selectRouteData - Add route title to
MinimalActivatedRouteSnapshot#title - Add route title selector:
RouterStore#title$ - Add type
MinimalRouteDatafor serializable route data
To keep route data serializable, we have removed support for the Angular Router's Data type's symbol index in MinimalActivatedRouteSnapshot#data. In particular, this is done to remove the Symbol(RouteTitle) entry added by the Angular Router for internal use. Use our MinimalRouteData type instead of Data from @angular/router for route data.
The provideGlobalRouterStore and provideLocalRouterStore functions now return an array of providers (Provider[]) instead of a single provider (Provider). No changes required in your providers metadata, for example the following usage remains the same.
@Component({
// (...)
providers: [provideLocalRouterStore()],
})
// (...)To support the stricter route title type introduced by the Angular Router, we now require at least the following peer dependencies.
- Require Angular 15.0
- Require
@ngrx/component-store15.0 - Require RxJS 7.4
- Require TypeScript 4.8
We have dropped TypeScript constructor parameter properties for ECMAScript compatibility, namely the useDefineForClassFields TypeScript compiler option is true (the default when targeting ES2022 or higher).
We have dropped TypeScript constructor parameter decorators for ECMAScript decorators compatibility.
- Remove type parameter from
selectQueryParam - Specify observable type returned from
selectQueryParam - Remove type parameter from
selectRouteParam - Specify observable type returned from
selectRouteParam
- Fixes #272 by building the package using Angular 14. Standalone components and applications are now fully supported, including in component tests.
To fully support standalone Angular applications and components, we now require at least the following peer dependencies.
- Require Angular 14.0
- Require
@ngrx/component-store14.0 - Require RxJS 7.4
- Require TypeScript 4.6
Signature before:
selectQueryParam<TValue>(param: string): Observable<TValue>;Signature after:
selectQueryParam(param: string): Observable<string | undefined>;Loose types now yield compilation errors. Remove the type parameter to use the
actual emitted type of string | undefined and optionally use operators to
change or narrow the type.
Before:
// Actual emitted values are of type `string | undefined` regardless of what we specify
const filter$ = routerStore.selectQueryParam<string | null>('filter');After:
// Emitted values are implicitly of type `string | undefined` and are only changeable through operators
const filter$ = routerStore.selectQueryParam('filter').pipe(map((filter) => filter ?? null));Signature before:
selectRouteParam<TValue>(param: string): Observable<TValue>;Signature after:
selectRouteParam(param: string): Observable<string | undefined>;Loose types now yield compilation errors. Remove the type parameter to use the
actual emitted type of string | undefined and optionally use operators to
change or narrow the type.
Before:
// Actual emitted values are of type `string | undefined` regardless of what we specify
const id$ = routerStore.selectRouteParam<number>('id');After:
// Emitted values are implicitly of type `string | undefined` and are only changeable through operators
const id$ = routerStore.selectRouteParam('id').pipe(
map(id => id === undefined ? undefined : Number(id),
);- Add
RouterStore - Remove
LocalRouterStore - Add
provideLocalRouterStore - Remove
GlobalRouterStore - Add
provideGlobalRouterStore
- Fix #272: Class constructor ComponentStore cannot be invoked without 'new'
We now require at least RxJS version 7.2 to import operators from the primary entry point of the rxjs package.
LocalRouterStore is replaced by RouterStore and provideLocalRouterStore.
Use provideLocalRouterStore() as component-level provider and inject RouterStore instead of LocalRouterStore.
Before:
// hero-detail.component.ts
// (...)
import { LocalRouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
providers: [LocalRouterStore],
})
export class HeroDetailComponent {
heroId$: Observable<string> = this.routerStore.selectQueryParam('id');
constructor(private routerStore: LocalRouterStore) {}
}After:
// hero-detail.component.ts
// (...)
import { provideLocalRouterStore, RouterStore } from '@ngworker/router-component-store';
@Component({
// (...)
providers: [provideLocalRouterStore()],
})
export class HeroDetailComponent {
heroId$: Observable<string> = this.routerStore.selectQueryParam('id');
constructor(private routerStore: RouterStore) {}
}GlobalRouterStore is replaced by RouterStore and provideGlobalRouterStore.
Add provideGlobalRouterStore() to your root environment injector and inject RouterStore instead of GlobalRouterStore.
Before:
// hero.service.ts
// (...)
import { GlobalRouterStore } from '@ngworker/router-component-store';
@Injectable({
providedIn: 'root',
})
export class HeroService {
activeHeroId$: Observable<string> = this.routerStore.selectQueryParam('id');
constructor(private routerStore: GlobalRouterStore) {}
}After:
// app.module.ts
// (...)
import { provideGlobalRouterStore } from '@ngworker/router-component-store';
@NgModule({
// (...)
providers: [provideGlobalRouterStore()],
})
export class AppModule {}// hero.service.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Injectable({
providedIn: 'root',
})
export class HeroService {
activeHeroId$: Observable<string> = this.routerStore.selectQueryParam('id');
constructor(private routerStore: RouterStore) {}
}- Encapsulate members inherited from
ComponentStore
- add
GlobalRouterStore - add
LocalRouterStore