DEV Community

Andrii Pap
Andrii Pap

Posted on

Writing Reactive and Declarative Code in Angular Using Signals

Introduction

With the introduction of signals in Angular, many of us are gradually adapting our patterns and habits. For years, RxJS was the default way to handle reactivity in Angular applications. While RxJS is powerful, it often comes with a steep learning curve and verbose code, especially for simple cases.

In this article, I’ll walk you through a common scenario: fetching a list. We’ll start with an imperative approach, then refactor it using RxJS for a more declarative and reactive style, and finally simplify it even further using Angular signals.


The Scenario: Fetching A Simple List

Imagine you have a list of items fetched from an API. You need to:

  • Fetch the list
  • Show it in the UI

No pagination, no complex logic — just the basics.


1️⃣ The Imperative Approach

This is often how we start: calling HTTP methods and updating local component state manually.

Service

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class ListService {
  #http = inject(HttpClient);

  fetchList() {
    return this.#http.get<Item[]>('/api/list');
  }
}
Enter fullscreen mode Exit fullscreen mode

Component

import { Component, OnInit, inject } from '@angular/core';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
})
export class ListComponent implements OnInit {
  #listService = inject(ListService);
  public list: Item[] = [];

  ngOnInit() {
    this.#listService.fetchList().subscribe((data) => {
      this.list = data;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Template

@for (item of list; track $index) {
  <div>{{ item.prop }}</div>
}
Enter fullscreen mode Exit fullscreen mode

✅ Observations

  • Control of ChangeDetectorRef is necessary if OnPush is used.

2️⃣ Reactive and Declarative with RxJS BehaviorSubject

Now, let’s move logic into the service. We’ll expose an observable so the component doesn’t handle subscriptions directly:

Service

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';

@Injectable()
export class ListService {
  #http = inject(HttpClient);
  #list = new BehaviorSubject<Item[]>([]);

  // Expose observable stream
  public list$ = this.#list.asObservable();

  fetchList() {
    this.#http.get<Item[]>('/api/list').subscribe((data) => {
      this.#list.next(data);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Component

import { Component, OnInit, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  imports: [AsyncPipe],
})
export class ListComponent implements OnInit {
  #listService = inject(ListService);
  public list$ = this.#listService.list$;

  ngOnInit() {
    this.#listService.fetchList();
  }
}
Enter fullscreen mode Exit fullscreen mode

Template

@let data = list$ | async;
@for (item of data; track $index) {
  <div>{{ item.prop }}</div>
}
Enter fullscreen mode Exit fullscreen mode

✅ Observations

  • Reactive and declarative: the component doesn’t manage state manually.
  • We rely on async pipe to skip manual change detection.

3️⃣ Simplified and Cleaner with Angular Signals

Now, let’s refactor using Angular signals:

Service

import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class ListService {
  #http = inject(HttpClient);
  #list = signal<Item[]>([]);
  public list = this.#list.asReadonly();

  fetchList() {
    this.#http.get<Item[]>('/api/list').subscribe((data) => {
      this.#list.set(data);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Component

import { Component, OnInit, inject } from '@angular/core';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
})
export class ListComponent implements OnInit {
  #listService = inject(ListService);
  public list = this.#listService.list;

  ngOnInit() {
    this.#listService.fetchList();
  }
}
Enter fullscreen mode Exit fullscreen mode

Template

@for (item of list(); track $index) {
  <div>{{ item.prop }}</div>
}
Enter fullscreen mode Exit fullscreen mode

✅ Observations

  • No BehaviorSubject or async pipe.
  • Signals automatically notify Angular when values change.
  • You don’t even need Zone.js.

❓ Why Bother?

For newcomers, RxJS can feel overwhelming. Signals give you a simpler entry point to write reactive code without fully committing to RxJS complexity.

That doesn’t mean signals replace RxJS in all scenarios — streams, complex operators, or multi-value flows are still RxJS territory. But for state management and simple list updates, signals just feel more natural and Angular-native.


Any Final Thoughts?

We still use RxJS for more complex cases, but here I wanted to show you the simplest setup just to get started. If you’d like to see a full CRUD example using a reactive approach, feel free to reach out or leave a comment.

Top comments (0)