YouTube

Within an imperative application, events invoke event handlers, and those handlers bundle together imperative logic. Regrettably, this pattern tends to dominate many Angular projects. You'll often encounter a method structured along these lines:

  navigateBack() {
    this.store.deleteMovies();
    this.store.switchFlag(false);
    this.router.navigate(['/']);
  }
Enter fullscreen mode Exit fullscreen mode

Within that event handler, three distinct state slices are being managed: store.movies, store.flag, and the application’s URL.

Imperative Callback

This approach falls short on separation of concerns, since the logic defining store.flag and store.movies gets distributed across numerous callbacks like navigateBack. As a result, tracing why a particular state exists at any moment forces you to hop between multiple spots in the codebase. In practice, this means leaning on "Find All References" constantly, which becomes tiresome.

Thus, eliminating event handlers and callbacks is a promising first step toward shifting the architecture toward reactivity.

What would that reactive implementation actually resemble?

A core principle for reactive design is this: Every user interaction in the template dispatches the smallest possible change to a single, centralized location in our TypeScript, and from there, all other state updates unfold reactively. The following diagram illustrates this pattern:

Reactive Source Diagram

The state-management logic now sits alongside the state it governs, which reduces the chance of introducing bugs—when writing new controls, we can draw directly on similar logic already in place.

So how do we translate this into actual code?

Let's assume we start by defining a subject that tracks back-button clicks:

backClick$ = new Subject<void>();
Enter fullscreen mode Exit fullscreen mode

With our observable in place, how should we handle all these imperative calls from the click handler?

    this.store.deleteMovies();
    this.store.switchFlag(false);
    this.router.navigate(['/']);
Enter fullscreen mode Exit fullscreen mode

The application I'm currently building relies on NgRx/Component-Store, a library that lacks built-in support for reactive state updates; it offers no hook for a click source observable, leaving us with class method calls instead.

As far as I can tell, there are 3 paths forward: build a bespoke wrapper for NgRx/Component-Store to accept observables; switch over to RxAngular/State; or move to StateAdapt, a library still pre-1.0. The winner here is clearly StateAdapt — I created it and I adore it 😀 (note: 1.0 lands in under a month 🤞)

Correction: NgRx/Component-Store does support observables for updating state, however I prefer my custom select override since it forwards subscriptions to the update observables exactly like StateAdapt.

Hold on — how about the router? For this method, there’s no reactive equivalent:

    this.router.navigate(['/']);
Enter fullscreen mode Exit fullscreen mode

In my ongoing series about advancing reactivity in Angular, I published a piece titled Wrapping Imperative APIs in Angular. There, I raised concerns about how the Angular ecosystem falls short when it comes to declarative APIs. To illustrate, Angular stands alone among the 9 leading front-end frameworks in that every dialog-opening and dialog-closing mechanism in its component libraries relies on imperative commands.

Framework Library 1 Library 2 Library 3
Vue ✅ Declarative ✅ Declarative ✅ Declarative
React ✅ Declarative ✅ Declarative ✅ Declarative
Svelte ✅ Declarative ✅ Declarative ✅ Declarative
Preact ✅ Declarative ✅ Declarative ✅ Declarative
Ember ✅ Declarative ✅ Declarative ✅ Declarative
Lit ✅ Declarative ✅ Declarative ✅ Declarative
SolidJS ✅ Declarative ✅ Declarative ---
Alpine ✅ Declarative --- ---
Angular ❌ Imperative ❌ Imperative ❌ Imperative

We don't have to settle for this just because it's been the Angular norm forever. A better approach is to introduce wrapper components that are usable declaratively in templates, hiding the imperative this.dialog.open() calls underneath.

Now, what about router state?

As far as I know, no front-end framework offers a native way to let the URL respond dynamically to application state. That alone doesn't prove the idea is doomed, but it might hint that it's worth reconsidering.

I've mulled this over extensively without reaching a verdict. Let's work through it here.

To start, it's unwise to design a single location where all navigation triggers from across the application are pulled together, merged into one stream, and fed into a declarative API. This approach would conflict with lazy-loading requirements, since many modules depend on on-demand loading, and a central importer would negate that benefit.

Instead, taking a cue from how declarative dialogs are structured might be the way forward: A central entity still manages the underlying state (the global modal element & backdrop), but a lightweight wrapper component can be dropped into any view, making it seem as though the modal originates within the component that opens it:

<h1>Some Component Template</h1>
<dialog [open]="dialogIsOpen$ | async">
  <app-dialog-content-component></app-dialog-content-component>
</dialog>
Enter fullscreen mode Exit fullscreen mode

Is a similar approach feasible for routing? Consider this:

<h1>Some Component Template</h1>
<button (click)="backClick$.next()">Back</button>
<app-navigate url="/" when="backClick$"></app-navigate>
Enter fullscreen mode Exit fullscreen mode

That actually brings to mind a thought...

<a routerLink="/">Back</a>
Enter fullscreen mode Exit fullscreen mode

Declarative navigation in the template turns out to be perfectly acceptable after all. The typical pattern, though, forms a narrow, self-contained cycle that runs directly from the user's click to the resulting route change. Is it acceptable to introduce a bit more flexibility into that cycle by using the app-navigate component?

Consider a scenario: a route is loaded, and then, for whatever reason, the application decides to leave that route. Would tracking down the cause be simpler if the navigate method were executed within the component class itself, instead of being triggered by a template component?

Of course, any template component could be invoking the navigate method behind the scenes anyway—the router is injectable from anywhere. Moreover, the wrapper component's selector being app-navigate makes its purpose immediately clear, doesn't it?

With that in mind, I'll build this wrapper component and observe what unfolds. Below is its complete source code:

import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import {
  BehaviorSubject,
  filter,
  Observable,
  of,
  switchAll,
  tap,
  withLatestFrom,
} from 'rxjs';

@Component({
  standalone: true,
  selector: 'app-navigate',
  template: '<ng-container *ngIf="navigate$ | async"></ng-container>',
  imports: [RouterModule, CommonModule],
})
export class NavigateComponent {
  @Input() set to(val: string) {
    this.toInput$.next(val);
  }
  toInput$ = new BehaviorSubject<string>('');

  @Input() set when(val: Observable<any>) {
    this.whenInput$.next(val);
  }
  whenInput$ = new BehaviorSubject<Observable<any>>(of(null));

  when$ = this.whenInput$.pipe(switchAll());
  navigate$ = this.when$.pipe(
    withLatestFrom(this.toInput$),
    filter(([, url]) => url !== ''),
    tap(([, url]) => this.router.navigate([url]))
  );

  constructor(private router: Router) {}
}
Enter fullscreen mode Exit fullscreen mode

That said, for this particular scenario, there’s no compelling need to rely on that component. Rather than funneling data through this pipeline:

Reactive Source Diagram

Why not simply make that back button a link and let the data travel in this direction instead?

Reactive Source 2 Diagram

We don't have to listen to backClick$ anymore. Instead, the other two methods can watch for the URL shifting back to home.

What's the trick for wiring those store methods up to that change?

Since both store methods need to respond to an observable, we face two paths: either migrate the entire store to StateAdapt immediately, or take a step-by-step route by employing the reactive utilities I outlined in that earlier piece.

We'll go with the incremental route.

Check out the source for the reactive wrapper class I built here, but the key point is that it makes this possible:

    this.react<AppStore>(this, {
      deleteMovies: urlFromMovieDetailToHome$.pipe(map(() => undefined)),
      switchFlag: urlFromMovieDetailToHome$.pipe(map(() => false)),
    });
Enter fullscreen mode Exit fullscreen mode

By importing ReactiveStore directly and extending it—rather than inheriting from ComponentStore—this behavior becomes available. As a result, each emission from urlFromMovieDetailToHome$ invokes the methods listed on the left side, forwarding the emitted value into each one.

To set up urlFromMovieDetailToHome$, I placed the router inside the Component Store class via injection and subscribed to its events, filtering for the specific navigation transition:

    const urlFromMovieDetailToHome$ = this.router.events.pipe(
      filter((event): event is NavigationEnd => event instanceof NavigationEnd),
      pairwise(),
      filter(
        ([before, after]) =>
          before.url === '/movie' && ['/home', '/'].includes(after.url)
      ),
    );
Enter fullscreen mode Exit fullscreen mode

This approach carries some complexity, though things will get easier once StateAdapt enters the picture.

With that in place, the update logic lives directly in the store next to the state it modifies. If we ever need to know why movies got deleted, a quick look at the class reveals exactly how urlFromMovieDetailToHome$ is defined! There's real value in declarative code. Sure, those update methods are technically imperative at heart, but I view the class as one cohesive unit, so for me that's declarative enough.

Only a single event has been converted to a reactive flow, yet the progress feels substantial. Here's what we've committed to so far:

  1. handle navigation reactively through a wrapper component around router.navigate
  2. transition NgRx/Component-Store toward reactive syntax step by step

Everything else in the component consists of standard downstream RxJS operations. The current code features nested subscriptions and a heavy dose of imperative statements:

  cast!: any[];
  movie!: MovieModel;

  // ...

  ngOnInit(): void {
    this.store.state$.subscribe((res) => {
      let movie = res.movieSelected;
      if (movie == null) {
        this.router.navigate(['/']);
      } else {
        this.singleMovie.getMovieDetails(movie.id).subscribe((data: any) => {
          this.movie = data;
          this.movie.poster_path = `${environment.imageUrl}${this.movie.poster_path}`;
        });
        this.singleMovie.getCast(movie.id).subscribe((data: any) => {
          this.cast = Array.from(data.cast);
          this.cast = this.cast.filter((c) => c.profile_path != null);
          this.cast.forEach((c) => {
            c.profile_path = `${environment.imageUrl}${c.profile_path}`;
          });
        });
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

With declarative navigation now in place, this turns out to be surprisingly straightforward.

On the store, there is an observable referenced as movieSelected$. From it, I can derive two additional observables:

  movieSelectedNull$ = this.store.movieSelected$.pipe(
    filter((movie) => movie == null)
  );
  movieNotNull$ = this.store.movieSelected$.pipe(
    filter((movie) => movie != null)
  ) as Observable<MovieModel>;
Enter fullscreen mode Exit fullscreen mode

We’ll pipe movieSelectedNull$ to the view so it kicks off the redirect to home:

<app-navigate to="/" [when]="movieSelectedNull$"></app-navigate>
Enter fullscreen mode Exit fullscreen mode

At this point, the non-null movie observable is available, which lets us set up movie$ as the reactive counterpart to the imperative movie property:

  movie$ = this.movieNotNull$.pipe(
    switchMap((movie) => this.singleMovie.getMovieDetails(movie.id)),
    map((data: any) => ({
      ...data,
      poster_path: `${environment.imageUrl}${data.poster_path}`,
    }))
  );
Enter fullscreen mode Exit fullscreen mode

At last, we are able to set up cast$, which serves as the reactive counterpart to the cast property found in the imperative approach:

  cast$ = this.movie$.pipe(
    switchMap((movie) =>
      this.singleMovie.getCast(movie.id).pipe(
        map((data: any) =>
          (Array.from(data.cast) as any[])
            .filter((c) => c.profile_path != null)
            .map((c) => ({
              ...c,
              profile_path: `${environment.imageUrl}${c.profile_path}`,
            }))
        )
      )
    )
  );
Enter fullscreen mode Exit fullscreen mode

Once the template is updated, that's all there is to it.

You can find the complete change set in this commit.

So far, the approach of kicking things off with events and then moving downstream is holding up nicely.

Before, the code contained 13 imperative statements and a jumble of unrelated responsibilities, as this color-highlighted screenshot makes clear:

info-movie.component-before

Much of that involved routine tidying, yet reactivity carried most of the weight, cutting imperative code down to zero and drawing clearer lines between responsibilities.

info-movie.component-after

There is less code visible here, but a significant portion was relocated to the store, and the template gained some additions. In total, the reactive version ended up being 5 lines longer. According to git stat, the commit recorded 63 insertions and 58 deletions.

The cleaner separation of concerns, however, makes this trade-off worthwhile. Once the entire app has been refactored for reactivity, I expect the reactive approach to be more concise overall.

Time will tell!


Meanwhile, explore StateAdapt!