LocalRouterStore
matchesActivatedRoute
more closely (#309)- Use
ActivatedRoute
to 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
MinimalRouteData
withStrictRouteData
(#319) - Change
RouterStore#routeData$
andMinimalActivatedRouteSnapshot#data
types fromData
toStrictRouteData
(#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-store
15.0 - Require RxJS 7.5
- Require TypeScript 4.8
- Ignore non-essential router events when serializing the router state. Only
NavigationStart
,RoutesRecognized
,NavigationEnd
,NavigationCancel
, andNavigationError
events 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
MinimalRouteData
for 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-store
15.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-store
14.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