ANGULAR START v19 has launched! ... Get 25% off LEARN MORE

đź‘‹ Hey there!

I'm Josh Morony, and this website is a collection of my free content centered around creating modern Angular applications using the latest Angular features, and concepts like reactive and declarative code.

Tutorial hero
Lesson icon

Handling Common Reactive/Declarative Scenarios with Signals and RxJS in Angular

Originally published July 12, 2023 Time 10 mins

I’ve been releasing videos on coding reactively with Angular for a long time now, and almost inevitably there are people who are convinced I am trying to fool them with pretty demos that don’t actually work in the “real world”.

There are also plenty of reasonable comments as well, and on my video recently about my new Signals and RxJS approach to state management, I had some requests for a more practical video focused on building, rather than the theory of the approach.

This article is adapted from the following video:

Reactive State Management with Angular

So, what I thought would be fun is to take all of the most common “gotcha” scenarios I’ve received on my videos over the years and show how they would be achieved with this approach.

Specifically, we are going to create a little app that:

  • Loads data from an API
  • Handles displaying errors
  • Handles displaying various loading/error/success states
  • Allows for manually triggered retries of failed requests - this is actually harder to do than automatic retries
  • Has paginated data
  • Allows for local filtering of data
  • Utilises valueChanges from Reactive Forms

Here’s what we are starting with - if you want to check out the full source code in detail you can find that here, but this is just the basic set up of the app. I’ve already got all the UI stuff in place, it’s just lacking all the data and logic to make it work, and that is what we are going to add in this ArticlesService here.

I’ve already set up some of the state management approach we discussed in the last video, but at the moment it is just the basic shape of the state we are interested in - I haven’t done any of the actual work in here yet. As you can see at the moment, the app doesn’t actually do anything useful.

Articles Service application

Populating Data from an API

So, let’s start by getting some data in here. First, we will set up a new source, and that source is going to pull data in from the dummy API service I have set up to simulate requesting data from an API.

articlesLoaded$ = this.apiServices.articles$;

Then in our constructor we will subscribe to that source, and say how it should update the state - in this case, we update the articles and set the status to "success". We add the takeUntilDestroyed operator to make sure we don’t have issues with leaking subscriptions.

constructor() {
// reducers
this.articlesLoaded$.pipe(takeUntilDestroyed()).subscribe((articles) =>
    this.state.update((state) => ({
    ...state,
    articles,
    status: "success",
    }))
);

Now we just make sure to pass in our articles selector to our list component.

@Component({
  standalone: true,
  selector: "app-articles",
  providers: [ArticlesService],
  template: `
    <app-search [control]="service.filterControl" />
    <app-list [articles]="service.filteredArticles()" />

Implementing Data Filtering

Now let’s see how we can filter the data. The trick here is that we want our data source for the search term in our service to be a stream of whatever the user is typing in the search field - but how do we get that stream of values from the search input to our service?

We don’t really want to introduce a manual subscribe to value changes in our component and have that pass the values to the service - ideally, we want the only place we are manually subscribing to anything to be for our reducers in the constructor here.

constructor() {
// reducers
this.articlesLoaded$.pipe(takeUntilDestroyed()).subscribe((articles) =>
    this.state.update((state) => ({
    ...state,
    articles,
    status: "success",
    }))
);

With a little sleight of hand, we can make this work really nicely. We just create a FormControl in the service, which allows us to directly access its valueChanges property from within the service.

filterControl = new FormControl();
// sources
articlesLoaded$ = this.apiService.articles$;
filter$ = this.filterControl.valueChanges;

Then we just pass that control into our search component, and bind it to the input we want to use for values.

<app-search [control]="service.filterControl" />

Now that we have access to the values, we can set up our data source, and our reducer that says how we want to update the state in response to the values coming from that source. In this case, all we want to do is set the filter value.

To actually filter the values, we are going to create a new filteredArticles selector and pass that into our list component instead of just the raw articles data.

// selectors
articles = computed(() => this.state().articles);
filter = computed(() => this.state().filter);
error = computed(() => this.state().error);
status = computed(() => this.state().status);
currentPage = computed(() => this.state().currentPage);

filteredArticles = computed(() => {
  const filter = this.filter();

  return filter ? this.articles().filter((article) => article.title.toLowerCase().includes(filter.toLowerCase())) : this.articles();
});

And now we have filtering.

Displaying and Handling Errors

Okay, now we also want to handle displaying errors. In a more simple, case this is very straightforward, we could just add an error handler to our subscribe and have that update the state with the error which is then just displayed in the template.

But we also want to create this manual retry feature where the user can click a button to attempt to reload the data if it fails.

As I hinted at before, it would actually be easier to just do an automatic retry with RxJS - but this is a specific scenario someone has challenged me with before so I want to show that it can be done with this approach, with relatively minimal complexity.

To test this, we are going to switch to this articlesFail stream I have set up that will randomly error. If we check the app now we can see this error is not being handled.

error not being handled

So let’s make a couple of changes to deal with this.

We have added a couple more sources now - a retry$ subject that we will trigger whenever we want to attempt a retry, and an error$ subject that we can manually pass an error.

retry$ = new Subject<void>();
error$ = new Subject<Error>();

This error source typically wouldn’t be required, we would normally just respond to the error coming from the stream directly. But we have a bit of a problem here.

We are using RxJS’s retry operator to retry loading the data if it fails, and in this case we are telling it to wait until our retry$ subject emits before retrying - this is what gives us this manual retry capability. But the problem is, with the retry operator our stream won’t actually error - it will just sit there waiting to retry - but we want to know if the fetch failed because we need to display the retry button for the user to click.

So, I’ve added in this extra imperative step of nexting the error subject within the delay so that we can actually get the error from the stream whilst it is waiting to retry.

retry$ = new Subject<void>();
error$ = new Subject<Error>();
articlesLoaded$ = this.apiService.articlesFail$.pipe(
  retry({
    delay: (err) => {
      this.error$.next(err);
      return this.retry$;
    },
  })
);

Then we just add some more reducers for our retry and error sources. retry$ will set the status back to loading, and the error$ source will set the status to error and also set the error message.

this.retry$.pipe(takeUntilDestroyed()).subscribe(() => this.state.update((state) => ({ ...state, status: 'loading' })));

this.error$.pipe(takeUntilDestroyed()).subscribe((error) =>
  this.state.update((state) => ({
    ...state,
    status: 'error',
    error: error.message,
  }))
);

Now we could have also just updated the state directly inside of the delay instead of introducing this new error$ source, but what I have done more strictly adheres to the idea of only imperatively modifying the state as a result of data sources emitting, rather than just updating state however and wherever we want, which can become messy.

imperatively modifying the state as a result of data sources emitting

Now we just need to update our retry button in the template to next the retry source when it is clicked.

Now if the data load fails we can just keep clicking retry until it succeeds. Again, keep in mind I don’t think this is a particularly good UX, I’d prefer the automatic retries which are easier, I just wanted to show that this was achievable.

<div *ngIf="service.status() === 'error'">
    <p>{{ service.error() }}</p>
    <button>Retry</button>
    <button (click)="service.retry$.next()">Retry</button>
</div>
</div>

Pagination Integration

Now let’s add in the pagination. We are going to have to modify our sources again.

Now we have introduced a source to update our currentPage, and we have changed our articles source to articlesForPage$. We now want our articles state to only contain the data for whatever page is currently loaded, so we take our currentPage value and use that to fetch the data from this dummy getArticlesByPage method I set up. This will just simulate pages by prefixing the data with whatever page we have requested. We also use startWith 1 because we want to load the first page automatically.

currentPage$ = new Subject<number>();

articlesForPage$ = this.currentPage$.pipe(
startWith(1),
switchMap((page) =>
    this.apiService.getArticlesByPage(page).pipe(
    retry({
        delay: (err) => {
        this.error$.next(err);
        return this.retry$;
        },
    })
    )
)

Now we will replace our articlesLoaded$ reducer with one for our new articlesForPage$ source and we will also add a reducer for the currentPage source as well.

this.articlesLoaded$.pipe(takeUntilDestroyed()).subscribe((articles) =>
        this.articlesForPage$.pipe(takeUntilDestroyed()).subscribe((articles) =>
      this.state.update((state) => ({
        ...state,
        articles,
        status: "success",
      }))
    );

    this.currentPage$
      .pipe(takeUntilDestroyed())
      .subscribe((currentPage) =>
        this.state.update((state) => ({ ...state, currentPage, status: "loading", articles: [] }))
      );

Now we just need to update our template to react to the pageChange events from the pagination component and make sure we next our currentPage data source.

<app-pagination [currentPage]="1" />
<app-pagination
    [currentPage]="service.currentPage()"
    (pageChange)="service.currentPage$.next($event)"
/>

Conclusion

So there you go - state management for data loading, error handling, manual retries, data filtering, and pagination all in a little over 100 lines of mostly reactive and declarative code that I think also doesn’t require all that much RxJS knowledge.

Learn to build modern Angular apps with my course