DEV Community

Cover image for Angular 16 is out now: Learn how to Replace RxJS with Signals
Daniel Glejzner for This is Angular

Posted on • Updated on • Originally published at Medium

Angular 16 is out now: Learn how to Replace RxJS with Signals

Angular 16 is out now: Learn how to Replace RxJS with Signals

Here I have one specific real world example for you, raw code comparison. Nothing more, nothing less!

But but but… Signals & RxJS right — it’s not supposed to replace it?

Couldn’t help myself and spark a little controversy in the title. Example I have is completely replacing RxJS implementation with Signals.

Signals are meant to replace RxJS to simplify your reactive code. However, only synchronous RxJS code — I wouldn’t replace asynchronous parts :).

Search & Pagination (RxJS)

Here is RxJS code of small feature that allows you to search for a user and use pagination. It’s meant to showcase how synchronous RxJS code can be simplified with Signals.

I asked for code review for this parts. While I have been talking with my friends — each one of them found different things to improve in my initial code and had different vision on how this could look like.

Additionally there have been occasional bugs and potential memory leaks. This is exactly why working with synchronous RxJS is not ideal. I also bet that looking at this example you also have a different kind of implementation in mind!

    const users = [
      { id: 1, name: 'Spiderman' },
      { id: 2, name: 'Hulk' },
      { id: 3, name: 'Wolverine' },
      { id: 4, name: 'Cyclops' },
      { id: 5, name: 'Venom' },
    ];

    @Component({
      selector: 'my-app',
      standalone: true,
      imports: [CommonModule, FormsModule],
      template: `
      <input [ngModel]="searchInput$ | async" (ngModelChange)="searchUser($event)" placeholder="Search">

      <ul>
        <li *ngFor="let user of paginatedAndFilteredUsers$ | async">{{ user.name }}</li>
      </ul>

      <button (click)="goToPrevPage()">Previous</button>
      pag. {{ currentPage$ | async }}
      <button (click)="goToNextPage()">Next</button>
    `,
    })
    export class App {
      readonly firstPage = 1;

      itemsPerPage = 2;

      searchInput$ = new BehaviorSubject('');
      currentPage$ = new BehaviorSubject(this.firstPage);

      paginatedAndFilteredUsers$ = combineLatest([
        this.currentPage$.pipe(distinctUntilChanged()), // trigger only when it actually changes
        this.searchInput$.pipe(
          distinctUntilChanged(),
          map((searchText) =>
            users.filter((user) =>
              user.name.toLowerCase().includes(searchText.toLowerCase())
            )
          )
        ),
      ]).pipe(
        map(([currentPage, filteredUsers]) => {
          const startIndex = (currentPage - 1) * this.itemsPerPage;
          const endIndex = startIndex + this.itemsPerPage;
          return filteredUsers.slice(startIndex, endIndex);
        })
      );

      searchUser(searchText: string): void {
        this.searchInput$.next(searchText);
        if (this.currentPage$.value > this.firstPage) {
          this.currentPage$.next(this.firstPage);
        }
      }

      goToPrevPage(): void {
        this.currentPage$.next(Math.max(this.currentPage$.value - 1, 1));
      }

      goToNextPage(): void {
        this.currentPage$.next(
          Math.min(this.currentPage$.value + 1, this.itemsPerPage + 1)
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

Search & Pagination (Signals)

Exactly the same implementation but done with Signals

    const users = [
      { id: 1, name: 'Spiderman' },
      { id: 2, name: 'Hulk' },
      { id: 3, name: 'Wolverine' },
      { id: 4, name: 'Cyclops' },
      { id: 5, name: 'Venom' },
    ];

    @Component({
      selector: 'my-app',
      standalone: true,
      imports: [CommonModule, FormsModule],
      template: `
      <input [ngModel]="searchInput()" (ngModelChange)="searchUser($event)" placeholder="Search">

      <ul>
        <li *ngFor="let user of paginatedAndFilteredUsers()">{{ user.name }}</li>
      </ul>

      <button (click)="goToPrevPage()">Previous</button>
      pag. {{ currentPage() }}
      <button (click)="goToNextPage()">Next</button>
    `,
    })
    export class App {
      readonly firstPage = 1;

      itemsPerPage = 2;

      searchInput = signal('');
      currentPage = signal(this.firstPage);

      paginatedAndFilteredUsers = computed(() => {
        const startIndex = (this.currentPage() - 1) * this.itemsPerPage;
        const endIndex = startIndex + this.itemsPerPage;
        return users
          .filter((user) =>
            user.name.toLowerCase().includes(this.searchInput().toLowerCase())
          )
          .slice(startIndex, endIndex);
      });

      searchUser(searchText: string): void {
        this.searchInput.set(searchText);
        if (this.currentPage() > this.firstPage) {
          this.currentPage.set(this.firstPage);
        }
      }

      goToPrevPage(): void {
        this.currentPage.update((currentPage) => Math.max(currentPage - 1, 1));
      }

      goToNextPage(): void {
        this.currentPage.update((currentPage) =>
          Math.min(currentPage + 1, this.itemsPerPage + 1)
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

Now comparison one by one

    //RxJs
    @Component({
      selector: 'my-app',
      standalone: true,
      imports: [CommonModule, FormsModule],
      template: `
      <input [ngModel]="searchInput$ | async" (ngModelChange)="searchUser($event)" placeholder="Search">

      <ul>
        <li *ngFor="let user of paginatedAndFilteredUsers$ | async">{{ user.name }}</li>
      </ul>

      <button (click)="goToPrevPage()">Previous</button>
      pag. {{ currentPage$ | async }}
      <button (click)="goToNextPage()">Next</button>
    `,
    })

    // Signals
    @Component({
      selector: 'my-app',
      standalone: true,
      imports: [CommonModule, FormsModule],
    template: `
      <input [ngModel]="searchInput()" (ngModelChange)="searchUser($event)" placeholder="Search">

      <ul>
        <li *ngFor="let user of paginatedAndFilteredUsers()">{{ user.name }}</li>
      </ul>

      <button (click)="goToPrevPage()">Previous</button>
      pag. {{ currentPage() }}
      <button (click)="goToNextPage()">Next</button>
    `,
    })
Enter fullscreen mode Exit fullscreen mode
    //RxJS
    readonly firstPage = 1;

      itemsPerPage = 2;

      searchInput$ = new BehaviorSubject('');
      currentPage$ = new BehaviorSubject(this.firstPage);

      paginatedAndFilteredUsers$ = combineLatest([
        this.currentPage$.pipe(distinctUntilChanged()),
        this.searchInput$.pipe(
          distinctUntilChanged(),
          map((searchText) =>
            users.filter((user) =>
              user.name.toLowerCase().includes(searchText.toLowerCase())
            )
          )
        ),
      ]).pipe(
        map(([currentPage, filteredUsers]) => {
          const startIndex = (currentPage - 1) * this.itemsPerPage;
          const endIndex = startIndex + this.itemsPerPage;
          return filteredUsers.slice(startIndex, endIndex);
        })
      );

    //Signals
    readonly firstPage = 1;

      itemsPerPage = 2;

      searchInput = signal('');
      currentPage = signal(this.firstPage);

      paginatedAndFilteredUsers = computed(() => {
        const startIndex = (this.currentPage() - 1) * this.itemsPerPage;
        const endIndex = startIndex + this.itemsPerPage;
        return users
          .filter((user) =>
            user.name.toLowerCase().includes(this.searchInput().toLowerCase())
          )
          .slice(startIndex, endIndex);
      });
Enter fullscreen mode Exit fullscreen mode
    //RxJS
    searchUser(searchText: string): void {
        this.searchInput$.next(searchText);
        if (this.currentPage$.value > this.firstPage) {
          this.currentPage$.next(this.firstPage);
        }
      }

    //Signals
    searchUser(searchText: string): void {
        this.searchInput.set(searchText);
        if (this.currentPage() > this.firstPage) {
          this.currentPage.set(this.firstPage);
        }
      }
Enter fullscreen mode Exit fullscreen mode
    //RxJS
    goToPrevPage(): void {
        this.currentPage$.next(Math.max(this.currentPage$.value - 1, 1));
      }

    goToNextPage(): void {
        this.currentPage$.next(
          Math.min(this.currentPage$.value + 1, this.itemsPerPage + 1)
        );
      }

    //Signals
    goToPrevPage(): void {
        this.currentPage.update((currentPage) => Math.max(currentPage - 1, 1));
      }

    goToNextPage(): void {
        this.currentPage.update((currentPage) =>
          Math.min(currentPage + 1, this.itemsPerPage + 1)
        );
      }
Enter fullscreen mode Exit fullscreen mode

Conclusion

Please remember that currently Signals are in developer preview only! However they are here to stay — and I have to say I love how they replace and simplify code initially written with synchronous RxJS in mind.

Disclaimer

If you are reading this in 2024 it means — I haven’t updated this content. You should find me and tell me how bad I behaved. Example here is based on initial developer preview of Signals available in Angular 16 on it’s release date.

This is going to change once full vision of Signals is available. If you tracked RFCs and proposals — you might immediately think that example presented would work differently with input based signals that are still not available to use.


I hope you liked my article!

If you did you might also like what I am doing on Twitter. I am hosting live Twitter Spaces about Angular with GDEs & industry experts! You can participate live, ask your questions or watch replays in a form of short clips :)

If you are interested drop me a follow on Twitter @DanielGlejzner — would mean a lot :). Thank You!

Top comments (6)

Collapse
 
railsstudent profile image
Connie Leung

Love your writing style, love how you compare the before version (RxJS) and after version (Signal)

Collapse
 
danielglejzner profile image
Daniel Glejzner

I’m glad you are enjoying it! Thank you :)

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Great article ,thanks. I am super excited about Signals, but I also do love RxJS :) Both have their place.

Collapse
 
danielglejzner profile image
Daniel Glejzner

Thanks! Same here :)

Collapse
 
alissonfpmorais profile image
Álisson Morais

Take everything that I'm going to say with a grain of salt, since I haven't read much about signals.

Both codes looks very similar to me. Yes there are changes, but they are minimal.

Also you're saying that signals is meant to be used for the synchronous part only (at least for now), so you still need to know RxJS, have a team that can use both libraries, maintaining both codes working and keep both libraries while shipping frontend code.

Am I missing something? Performance or maybe the examples are just small that I can see much difference? Because right now it does not look very promising to me.

Collapse
 
danielglejzner profile image
Daniel Glejzner • Edited

Hello! I truly appreciate every comment and thank you for taking the time to share your thoughts.

I highly recommend staying up-to-date with the latest developments, as this will help clarify the evolving vision for Angular's future. You might find the following articles that I have written insightful for more details:

dev.to/this-is-angular/sub-rfc-4-f...
dev.to/this-is-angular/signals-sub...
dev.to/this-is-angular/minko-geche...

In brief:

The example provided is intentionally simplistic while still representing a real-world scenario. It is designed to illustrate the differences between Signals and RxJS code on a small scale.

Keep in mind that this is just a glimpse of the larger picture. Signals are planned to eventually replace zone.js change detection, with Signal-based components featuring Inputs based on Signals.

RxJS/Signals interoperability is already in place. Replacing RxJS for synchronous logic is not the primary goal here , but rather a part of bigger picture.