Pushing Angular's Reactive Limits: Event Context & Toasts/Dialogs
Tags: angular, ngrx, stateadapt
Published: 2022-11-16 16:38:25

---
title: Pushing Angular's Reactive Limits: Event Context & Toasts/Dialogs
published: true
description: I converted a component from an imperative approach using NgRx/Component-Store to a completely reactive approach. I added local state to an event's payload to keep local and global state separate. I also created a declarative toast wrapper component.
tags: angular,ngrx,stateadapt
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqq1gvuyuopagdhec08y.png
# Use a ratio of 100:42 for best results.
# published_at: 2022-11-12 09:00 +0000
series: Pushing Angular's Reactive Limits
---

[YouTube](https://youtu.be/_wBvJi_okyQ)

In an imperative app, events trigger event handlers, which are containers of imperative code. This seems to be the most common approach in Angular apps, unfortunately. A typical app might have a function like this:

```tsx
  onSubmit() {
    if (this.form.valid) {
      this.store.deleteMovies();
      this.headerService
        .searchMovies(this.form.value)
        .subscribe((data: any) => {
          // ...
          this.store.saveSearch(data.results);
          this.store.saveSearchHeader(this.form.value);
          this.store.switchFlag(true);
          this.router.navigate(['/search']);
        });
    } else {
      swal({ // toast
        title: 'Incorrecto',
        // ...
      });
    }
  }
}
```

That event callback is controlling 5 pieces of state: 
- `store.search`
- `store.header`
- `store.flag`
- the app's URL
- the `invalid` toast

![Before Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sftzs7om3ezce8fclud4.png)

This isn't great separation of concerns, because logic that controls each of those 5 pieces of state is scattered across many callbacks such as `onSubmit`, so to understand why anything is the way it is at a given time, you have to refer to many locations across the codebase. Basically, you need "Find All References", which can get really annoying.

So getting rid of event handlers/callbacks seems like a good potential place to start refactoring to more reactivity.

And what would the reactive version look like?

A simple rule for reactivity is this: **Every user event in the template pushes the most minimal change to a single place in our TypeScript, then everything else reacts to that.** Here's a diagram of that:

![After Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76fdveb5e46bkui9j5gm.png)

Not only is the code immediately downstream from the event source much simpler, but everything that can be downstream from something else is. This maximizes reactivity. In general, diagrams of reactive data flows will be taller and skinnier than diagrams of imperative data flows. You can actually see the improved separation of concerns. 

(I haven't looked into this yet, but I suspect that even `store.header` and `store.flag` might be downstream from `store.searchResults`.)

The logic that controls each piece of state has been moved next to the state it controls. This makes it easier to avoid bugs, since we can easily refer to similar logic when writing new logic to control state.

# Reactive Implementation

First, let's say we create a subject to represent the form submission:

```tsx
search$ = new Subject<[boolean, string]>();
```

```html
        <input
          type="submit"
          value="Buscar"
          class="submit"
          (click)="search$.next([form.valid, form.value.search])"
        />
```

Why did I give it a payload whereas the previous event handler `onSubmit` took no parameters?

# Event Context

The Reason to pass the form data into the `search$` event is flexibility: The previous event handler actually needed `form.valid` and `form.value`; it was just implicitly referenced. This makes it require refactoring if a developer wanted to move it to a service or something. 

One problem people often have with reactive state management like [StateAdapt](https://state-adapt.github.io/) or [NgRx](https://ngrx.io/) is that state is siloed into independent slices (or reducers, or stores). Sometimes in order to calculate a state change, you need to know the state of another state reducer/store. So what do you do? Refactor everything into one giant reducer/store?

The simple answer is to provide context with the event that is causing the state change. 

# Navigation

We now have 2 pieces of state immediately downstream from `search$`:

![Search to URL and Toast](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sy58ktx35qwnkid41jrn.png)

Like in the previous component, we need to split our stream into 2 pieces again. Again, I'll just use `filter` instead of `partition`.

Also... It's much more convenient to pass in a URL string and have `app-navigate` just react to that. In the last article I was thinking through this for the first time, and I believe I came up with an API that is less than ideal. So, rather than having a separate observable to trigger the `router.navigate` call, the mindset will be state-centric: The job of the router wrapper component is to ensure that the browser URL is in sync with the `url` input passed into it. 

First, here's how I define the observables that chain off of `search$`:

```tsx
  search$ = new Subject<[boolean, string]>();

  searchIsInvalid$ = this.search$.pipe(map(([valid]) => !valid));
  url$ = this.search$.pipe(
    filter(([valid]) => valid),
    map(([, search]) => `search/${search}`)
  );
```

Now I can feed `url$` into the `app-navigate` component:

```html
<app-navigate [url]="url$ | async"></app-navigate>
```

Also, here's the new source code for that component:

```tsx
import { Component, Input, SimpleChanges } from '@angular/core';
import { Router, RouterModule } from '@angular/router';

@Component({
  standalone: true,
  selector: 'app-navigate',
  template: '',
  imports: [RouterModule],
})
export class NavigateComponent {
  @Input() url: string | null = null;

  constructor(private router: Router) {}

  ngOnChanges(changes: SimpleChanges) {
    const urlChange = changes['url'];
    const newUrl = urlChange?.currentValue;
    if (newUrl) {
      this.router.navigate([newUrl]);
    }
  }
}
```

That's actually smaller and simpler than it used to be. I like it.

Now I'll refactor the component from the last article to map to a URL instead of `selectedMovieNull$`:

```tsx
  url$ = this.store.movieSelected$.pipe(
    filter((movie) => movie == null),
    map(() => '/')
  );
```

I added a route parameter to the `search` route, since search is being tracked there now.

And I just discovered that the `store.header` state was the search query string. So apparently I have merged it in with the URL, and I can get rid of it in the store. It actually was never being used anywhere anyway, it turns out. I love refactoring random projects from GitHub :)

# Toasts

I'm only now realizing that `swal` actually means "Sweet Alert" and this is a modal being opened, not a toast. But it doesn't matter. Everything in the UI should be declarative, and this might as well be a toast. 

So let's make a wrapper component we can pass into a `swal` function call:

```tsx
import { Component, Input, SimpleChanges } from '@angular/core';
import { SwalParams } from 'sweetalert/typings/core';
import swal from 'sweetalert';

@Component({
  standalone: true,
  selector: 'app-swal',
  template: '',
})
export class SwalComponent {
  @Input() show: boolean | null = false;
  @Input() options: SwalParams[0] = {};

  ngOnChanges(changes: SimpleChanges) {
    const showChange = changes['show'];
    const newShow = showChange?.currentValue;
    if (newShow) {
      swal(this.options);
    }
  }
}
```

This could evolve over time, but this is good enough for our purposes. 

Now I can use it in the template and the search/header component is completely converted to reactivity:

```html
        <app-swal
          [options]="{
            title: 'Incorrecto',
            text: 'Debes ingresar al menos dos caracteres para hacer una búsqueda..',
            icon: 'warning',
            dangerMode: true
          }"
          [show]="searchIsInvalid$ | async"
        ></app-swal>
```

This has a bug apparently. It only opens once, then never opens again. This is because we are not resetting the state we're passing into the `swal` wrapper component, so `ngOnChanges` is not reacting to it.

In order for this to work correctly, we'd need to handle a callback function from the `swal` alert and have it update an observable to be merged back into its input so a `false` value would be passed in. 

I decided to go ahead and implement this. Here's the new `swal` wrapper component:

```tsx
import {
  Component,
  EventEmitter,
  Input,
  Output,
  SimpleChanges,
} from '@angular/core';
import { SwalParams } from 'sweetalert/typings/core';
import swal from 'sweetalert';

@Component({
  standalone: true,
  selector: 'app-swal',
  template: '',
})
export class SwalComponent {
  @Input() show: boolean | null = false;
  @Input() options: SwalParams[0] = {};
  @Output() close = new EventEmitter<any>();

  ngOnChanges(changes: SimpleChanges) {
    const showChange = changes['show'];
    const newShow = showChange?.currentValue;
    if (newShow) {
      swal(this.options).then((value) => this.close.emit(value));
    }
  }
}
```

Now we need to change the state we pass in to always reflect the state of whether that alert is open.

In this case, the simplest way of tracking the dialog state I can think of is using [StateAdapt](https://state-adapt.github.io/). This is because it needs to react to an observable `searchIsInvalid$` but also be able to be set directly from the template. Remember, from the template, something imperative always needs to happen anyway, so we might as well be direct with it.

To use [StateAdapt](https://state-adapt.github.io/), we first need this in our `AppModule`:

```tsx
import { defaultStoreProvider } from '@state-adapt/angular';
// ...
  providers: [defaultStoreProvider],
```

Now we can use it in the component like this:

```tsx
import { booleanAdapter } from '@state-adapt/core/adapters';
import { toSource } from '@state-adapt/rxjs';
import { adapt } from '@state-adapt/angular';

// ...

  searchIsInvalid$ = this.search$.pipe(
    map(([valid]) => !valid),
    toSource('searchIsInvalid$')
  );
  invalidAlertOpen = adapt(
    ['invalidAlertOpen', false, booleanAdapter],
    this.searchIsInvalid$
  );
```

```html
        <app-swal
          [options]="{
            title: 'Incorrecto',
            text: 'Debes ingresar al menos dos caracteres para hacer una búsqueda..',
            icon: 'warning',
            dangerMode: true
          }"
          [show]="invalidAlertOpen.state$ | async"
          (close)="invalidAlertOpen.setFalse()"
        ></app-swal>
```

And as an added benefit, we get this state tracked in Redux Devtools:

![invalid-alert-redux-devtools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/643idhttxy55a8ooetgz.png)

How weird is that? The first state tracked in Redux Devtools in this entire app is an alert dialog, and it wasn't even managed as state before. 

# Downstream NgRx/Component-Store Changes

Alright, the component is working great, now we need to move on to the next level downstream:

![url$-to-results$-&-store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59npk5tjnuwr9wmncwxr.png)

This is all going to be defined in `app.store.ts`, because that's what it affects. 

First let's trigger `deleteMovies` when the route is first entered. There's some logic we previously defined that I'm going to split out so we can reuse it. Here's what I have now:

```tsx
    // New
    const urlAfterNav$ = this.router.events.pipe(
      filter((event): event is NavigationEnd => event instanceof NavigationEnd),
      map((event) => event.url)
    );

    // From before:
    const urlFromMovieDetailToHome$ = urlAfterNav$.pipe(
      pairwise(),
      filter(
        ([before, after]) =>
          before === '/movie' && ['/home', '/'].includes(after)
      )
    );

    // New
    const searchRoute$ = urlAfterNav$.pipe(
      filter((url) => url.startsWith('/search'))
    );
```

Nice. 

I'm getting a bug as well. It turns out `switchFlag` needs to be called immediately too. So, I'm going to trigger both state reactions with this new observable:

```tsx
    this.react<AppStore>(this, {
      deleteMovies: merge(urlFromMovieDetailToHome$, searchRoute$).pipe(
        map(() => undefined)
      ),
      switchFlag: merge(
        urlFromMovieDetailToHome$.pipe(map(() => false)),
        searchRoute$.pipe(map(() => true))
      ),
    });
```

If you're confused about this block of code, see the last article where I explained `react`. Hopefully it's somewhat self-explanatory, but just know that the keys of the object passed in are names of state change functions in this NgRx/Component-Store, and they get called and passed the values emitted from the observables on the right-hand side.

Now we just need to define the `results$` observable as chaining off of the search term in the URL:

```tsx
    const searchResults$ = searchRoute$.pipe(
      switchMap((url) => {
        const [, search] = url.split('/search/');
        return this.headerService.searchMovies({ search });
      }),
      map((res: any) =>
        res.results.map((movie: any) => ({
          ...movie,
          poster_path:
            movie.poster_path !== null
              ? `${environment.imageUrl}${movie.poster_path}`
              : 'assets/no-image.png',
        }))
      )
    );
```

This takes the URL, grabs the search query from it, passes it to the `MovieService` and loops through the results giving each movie a default poster image.  This logic existed already, including those unfortunate `any` types. I don't feel like fixing that right now. 

And we can plug this observable into the `react` method now:

```tsx
saveSearch: searchResults$,
```

`saveSearch` should probably be named `receiveSearchResults`, but I'm going to ignore that for now too.

# Conclusion

Everything works again! And it's reactive now! [Here's the final commit.](https://github.com/mfp22/ngrx-cs-stateadapt-sylomovies/commit/6741071787c144f2b832e47d0fa000f3400baf29)

The main takeaway from this is the lesson I learned from creating the `SwalComponent` and refactoring the `AppNavigateComponent`: **Angular inputs should represent state, not events.** This is more declarative anyway, so I'm happy with the way these wrapper components are now. 

Yes, it does take work to create these wrapper components and integrate them properly. But it enables a much more flexible data flow with better separation of concerns. 

Previously, there were 11 imperative statements, and lots of mixed concerns, as you can see from this screenshot:

![Before](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkq8zv8z6br1c3vyjsys.png)

The new reactive implementation only has 2 imperative statements, both from the template, so, unavoidable. But the reactive data flow introduced better separation of concerns, as you can see:

![after-header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9trhm7jjpvv0xt41xpa7.png)
![after-store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ogilqim6k8u94zwvvk7.png)

Notice here that we now have multiple sources of state changes in one place, including the state changes we added when refactoring the first component. At the end of this refactor project, maybe I should come up with a way to show visually how across multiple files, state change logic became centralized next to the state it was changing. That might be cool. 

Another thing to notice is that I'm not showing all the code I created while refactoring. That is because it is represented in the variable name instead. If you look at just this line:

```tsx
      saveSearch: searchResults$,
```

You know that the state will change as described by `saveSearch` when `searchResults$` emits. The name `searchResults$` tells you what you need to know, and if you want to know more details, you can use "Click to Definition" and look at it.

Everything that will cause search results to be updated will be right here. So far this is the only one. But if we add another, it might look like 

```tsx
      saveSearch: merge(searchResults$, moreSearchResults$),
```

On the other hand, the only way to know why search results changed in an imperative project would be to use `Find All References` on the `saveSearch` method. You don't even know the names of the callback functions that are controlling it until you do that. 

Not only that, but a callback function like `onSubmit` gives you no clue what it actually does or what state it modifies. It's a bad name for a function; ideally you could have something in the template like `(submit)="saveToServer()"`, but what if that's not the only thing that has to happen? With callback functions, which are containers of imperative code, you can only give it the name of the commonality of all of its effects. It happens to be that quite often the only name people can come up with is the circumstance or context in which the callback function is called. This violates Clean Code's advice for good function names, which says that functions should be named for *what they do*.

Good names come more easily with declarative programming, because the things you have to name are only concerned with themselves. 

So, how much code was that? `git diff --stat` says 79 insertions and 62 deletions. 

I'm beginning to doubt that this will be less code by the end. I increased the lines of code a lot when I changed 

```tsx
      swal({
        title: 'Incorrecto',
        text: 'Debes ingresar al menos dos caracteres para hacer una búsqueda..',
        icon: 'warning',
        dangerMode: true,
      });
```

to

```tsx
  searchIsInvalid$ = this.search$.pipe(
    map(([valid]) => !valid),
    toSource('searchIsInvalid$')
  );
  invalidAlertOpen = adapt(
    ['invalidAlertOpen', false, booleanAdapter],
    this.searchIsInvalid$
  );
```

and 

```html
        <app-swal
          [options]="{
            title: 'Incorrecto',
            text: 'Debes ingresar al menos dos caracteres para hacer una búsqueda..',
            icon: 'warning',
            dangerMode: true
          }"
          [show]="invalidAlertOpen.state$ | async"
          (close)="invalidAlertOpen.setFalse()"
        ></app-swal>
```

That's going from 6 to 18 lines of code. That's most of the difference, actually. 

But since we now have something in Redux Devtools, and we separated the concern of the dialog from the other updates that `onSubmit` was burdened with, I think this was worth it. 

I guess we'll see how things compare in the end! There are still 2-3 components to refactor.