Skip to content

Releases: ngworker/router-component-store

@ngworker/router-signal-store 17.0.0

23 Sep 23:56
9cc62d0

Choose a tag to compare

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).

Features

  • RouterSignalStore shared API for dependency injection
  • provideLocalRouterSignalStore for providing a local router signal store, a replacement for ActivatedRoute
  • provideGlobalRouterSignalStore for providing the global router signal store, a replacement for NgRx Router Store
  • Strong and strict typing with StrictQueryParams, StrictRouteData, and StrictRouteParams
  • Serializable router state with MinimalActivatedRouteSnapshot

Compatibility

  • Require Angular 17.0
  • Require @ngrx/signals 17.0
  • Require TypeScript 5.2

17.0.0

16 Sep 19:46
2eeb933

Choose a tag to compare

Features

Compatibility

  • Require Angular 17.0
  • Require @ngrx/component-store 17.0
  • Require TypeScript 5.2

16.0.0

19 Aug 16:01
a70dc20

Choose a tag to compare

Features

  • Add RouterStore#selectRouteDataParam(param: string) to match NgRx Router Store selector name (#341)

Deprecations

  • Deprecate RouterStore#selectRouteData
    • Use RouterStore#selectRouteDataParam instead
    • To be removed in version 18

BREAKING CHANGES

Compatibility

  • Require Angular 16.0
  • Require @ngrx/component-store 16.0
  • Require RxJS 7.5
  • Require TypeScript 4.9

15.0.0

15 Aug 08:26
b0371a7

Choose a tag to compare

First stable release. No functional or API changes from 15.0.0-rc.2.

Features

Provide local or global router store using provideLocalRouterStore or provideGlobalRouterStore, respectively

  • A local router store matches the ActivatedRoute service's observable properties and follow the lifecycle of the component that provides it
  • The global router store matches the @ngrx/router-store selectors 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.

15.0.0-rc.2

12 Feb 20:48
ad102e1

Choose a tag to compare

15.0.0-rc.2 Pre-release
Pre-release

Features

  • Use StrictQueryParams for query parameters instead of StrictRouteParams (#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 uses StrictQueryParams

RouterStore#selectQueryParam uses 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]))
    );
}

15.0.0-rc.1

17 Sep 19:15
6ac16c9

Choose a tag to compare

15.0.0-rc.1 Pre-release
Pre-release

Refactors

  • Replace StrictRouteParams with simple type (#325)
  • Replace StrictRouteData with simple type (#326)

15.0.0-rc.0

03 Sep 14:36
8228d7e

Choose a tag to compare

15.0.0-rc.0 Pre-release
Pre-release

Features

  • LocalRouterStore matches ActivatedRoute more closely (#309)
    • Use ActivatedRoute to serialize the router state for the local router store implementation (LocalRouterStore)
    • LocalRouterStore.currentRoute$ matches ActivatedRoute.snapshot
  • Remove optional type parameter from RouterStore#selectRouteData (#316)
  • Replace MinimalRouteData with StrictRouteData (#319)
  • Change RouterStore#routeData$ and MinimalActivatedRouteSnapshot#data types from Data to StrictRouteData (#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

0.3.2

03 Jan 22:38
e9d90fa

Choose a tag to compare

0.3.2 Pre-release
Pre-release

Performance optimizations

  • Ignore non-essential router events when serializing the router state. Only NavigationStart, RoutesRecognized, NavigationEnd, NavigationCancel, and NavigationError events are essential.

0.3.1

02 Jan 23:27
33606a6

Choose a tag to compare

0.3.1 Pre-release
Pre-release

Features

  • Add factory for selecting router events of specific types: RouterStore#selectRouterEvents

0.3.0

19 Dec 09:31
101c16f

Choose a tag to compare

0.3.0 Pre-release
Pre-release

Features

  • 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

BREAKING CHANGES

Remove symbol keys from 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.

Provider factories return provider arrays

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()],
})
// (...)

Compatibility

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.