DEV Community

Cover image for How to refactor to reactivity
Mike Pearson for This is Angular

Posted on

How to refactor to reactivity

I just finished rewriting an app from imperative code to completely reactive code. Here's the process I followed.

1. The first event handler

I started with a single event and removed its handler. The handler was modifying the URL and 2 pieces of state in the global NgRx/Component-Store. So, I changed the (click)to a [routerLink] and had the state changes react to the URL change:

    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)
      ),
    );

    // ...

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

This react method turned out to be unnecessary... I missed the part in NgRx/Component-Store's documentation that says updates can take in observables. So it could have been this:

    this.deleteMovies(urlFromMovieDetailToHome$.pipe(map(() => undefined)));
    this.switchFlag(urlFromMovieDetailToHome$.pipe(map(() => false)));
Enter fullscreen mode Exit fullscreen mode

Anyway.

Do you see that weird URL code I wrote? It's totally fine, and it worked. But maybe it should have been an early sign that the state reacting to the URL should have been in the route itself and not in a global store. But I didn't do anything with that, because there were still other event handlers modifying it from other components. And it was totally fine working the way it was.

2. More event handlers

I went from from component to component converting them to reactivity, but each component happened to only have one event handler, so it ended up being the same as moving from handler to handler. I actually think it would be better in general to move from handler to handler and worry about the rest of the component code later.

After I converted a couple of event handlers to have state react on its own instead, I noticed there was nothing preventing me from moving some state out of the global store and colocating it with the feature that was using it. I also noticed that multiple pieces of state would always react to the same things, and some of it could be turned into derived state, which is downstream from (reacts to) other state. This is easy to see when all the code that controls state is colocated with the state.

At this point the code began to simplify a lot.

After I converted the last component to reactivity, I realized that the global store wasn't even necessary. So I moved state into the related features, and it ended up being 32% less code in total.

Although there are probably many ways to approach this process, this is what I like:

  1. Find an event handler
  2. Delete it
  3. Have the event make a single change instead
  4. Get the next state changes to react to that immediate, single change
  5. Find the state with the fewest remaining event handlers changing it, move to one of them and repeat steps 2-4
  6. If all of a state's old handlers are gone, move it as close as possible to the features that use it
  7. Repeat steps 1-6 for the features using the state that is now completely reactive until all its features are reactive

3. Wrapping imperative APIs

Some features have imperative APIs. In order to completely get rid of imperative code, you need to wrap these APIs with declarative APIs.

In the first 2 articles in this series I described how I wrapped 2 imperative APIs with declarative ones.

The first was router.navigate. I created a wrapper component that can be used like this:

<app-navigate [url]="url$ | async"></app-navigate>
Enter fullscreen mode Exit fullscreen mode

The second was a Sweet Alerts wrapper component that can be used like this:

<app-swal
  [options]="{}"
  [show]="alertIsVisible$ | async"
  (close)="alertClosed$.next()"
></app-swal>
Enter fullscreen mode Exit fullscreen mode

There was a 3rd imperative API I haven't described yet: The image carousel. It used to be controlled like this:

  @ViewChild('carousel', { static: true }) carousel!: NgbCarousel;
// ...
  togglePaused() {
    if (this.paused) {
      this.carousel.cycle();
    } else {
      this.carousel.pause();
    }
    this.paused = !this.paused;
  }
Enter fullscreen mode Exit fullscreen mode

I realized I could just change the interval it was looping by instead (got the idea originally from Josh Moroney actually; I think it's great):

  pausedAdapter = buildAdapter<boolean>()(booleanAdapter)({
    interval: (s) => (s.state ? 9999999 : this.config.interval),
  })();
Enter fullscreen mode Exit fullscreen mode

This is StateAdapt syntax by the way. interval becomes available as an observable on the store that uses pausedAdapter. Here's that store:

  paused = adapt(['carousel.paused', false, this.pausedAdapter], {
    setFalse: this.arrow$,
    setTrue: this.indicator$,
  });
Enter fullscreen mode Exit fullscreen mode

And here I'm using it in the template:

  [interval]="(paused.interval$ | async) || config.interval"
Enter fullscreen mode Exit fullscreen mode

Using declarative APIs for these features was extremely nice. I'm so sick of interrupting my nice, clean RxJS streams with manual subscriptions for imperative APIs. Let's do something about it!

Wrap All The APIs!

Diagrams

Here are diagrams that show the data flow before and after I refactored the app. To see me walk through these in detail, watch the YouTube video below the diagrams.

Before Diagram

After Diagram


Thanks for reading! Please give StateAdapt a look if you haven't yet. Version 1.0 is coming out within a month!

Top comments (0)