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

You get a single, concrete, real-life case here—just a direct code comparison. No extras, no fluff.

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

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

The title was a deliberate bait to stir up some debate—and I admit I couldn’t resist. My example fully swaps out RxJS for Signals.

Signals should simplify reactive code and claim RxJS’s role, but only regarding synchronous operations — asynchronous flows should stay untouched :).

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

Search & Pagination (RxJS)

A compact feature enables user lookup with pagination, implemented using RxJS. Its purpose is to demonstrate how synchronous RxJS code can be simplified with Signals.

When I sought feedback on this code from my peers, each one of them found different things to improve in my initial code and had different vision on how this could look like.

On top of that, sporadic defects appeared and potential memory leaks lurked. This is exactly why working with synchronous RxJS is not ideal. Chances are, seeing this snippet, you've already pictured a distinct approach for the solution.

    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

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

Search & Pagination (Signals)

The identical pipeline, yet built on Signals instead.

    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

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

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

Signals remain in developer preview, so keep that in mind. They’re clearly part of Angular’s future, and honestly, I’m impressed with how they streamline and simplify code that originally depended on synchronous RxJS patterns.

Disclaimer

Reading this in 2024? Then please flag me down and call me out — I haven’t touched this content since it was written. The example draws on the initial Signals developer preview shipped with Angular 16 at its launch.

Things will shift once the full Signals design is rolled out. If you’ve been following the RFCs and proposals, you’d likely notice that this example would change with input-based signals, which aren’t available yet.


I hope you liked my article!

Enjoyed it? Then you might appreciate what I’m up to on Twitter. I run live Twitter Spaces on Angular, featuring GDEs & industry pros — join in to ask questions live, or catch replays as short clips afterward.

Interested? Give me a follow at Twitter @DanielGlejzner — it means a lot. Thanks!