Angular 19 avec Signals et Resource API : créer des applications réactives en 2026

Noqta Team
Par Noqta Team ·

Chargement du lecteur de synthèse vocale...

Angular 19 change tout ce que vous pensiez savoir de ce framework. Les Signals remplacent RxJS pour la gestion d'état, la Resource API transforme les données asynchrones en primitives réactives de première classe, et la détection sans Zone.js offre des performances comparables à React. Dans ce tutoriel, vous construirez un dashboard réactif complet et maîtriserez la stack Angular moderne dès le premier jour.

Ce que vous allez apprendre

À la fin de ce tutoriel, vous serez capable de :

  • Créer des composants standalone sans NgModules
  • Utiliser les Signals (signal, computed, effect) pour l'état réactif
  • Récupérer des données distantes avec la nouvelle Resource API et httpResource
  • Exécuter Angular en mode zoneless pour des performances maximales
  • Construire des formulaires basés sur Signals avec sûreté de types
  • Faire le pont entre observables RxJS et Signals avec toSignal et toObservable
  • Déployer une application Angular prête pour la production

Prérequis

Avant de commencer, assurez-vous de disposer de :

  • Node.js 20 ou plus récent (Angular 19 exige Node 20 minimum)
  • Des bases en TypeScript (interfaces, generics, décorateurs)
  • Une aisance avec HTML et CSS
  • Un éditeur, VS Code avec l'extension Angular Language Service recommandé
  • Une expérience de l'Angular CLI utile mais non obligatoire

Pourquoi Angular 19 compte

Pendant des années, Angular a paru lourd face à React ou Vue. Les NgModules imposaient du code répétitif, Zone.js monkey-patchait les globales pour déclencher la détection de changements, et RxJS restait la seule voie idiomatique pour l'asynchrone. Angular 19 corrige tout cela :

  • Standalone par défaut — plus de NgModules, des imports plus propres
  • Signals — réactivité fine sans la surcharge de Zone.js
  • Détection de changements sans Zone — bundles plus petits, rendu plus rapide
  • Resource API — récupération de données réactive avec états de chargement et d'erreur intégrés
  • Hydratation incrémentale — pages rendues côté serveur hydratées à la demande

Le résultat est un Angular moderne, léger et véritable alternative aux React Server Components.

Ce que vous allez construire

Un véritable Signals Dashboard affichant des statistiques de dépôts GitHub. L'application va :

  • Récupérer des dépôts via l'API GitHub avec httpResource
  • Les filtrer et les trier de manière réactive avec des Signals
  • Persister les préférences utilisateur dans un store basé sur Signals
  • Mettre à jour l'interface instantanément sans aucun ngIf ou ngFor, grâce à la nouvelle syntaxe de contrôle @if et @for

Étape 1 : créer le projet Angular 19

Installez le dernier Angular CLI et générez un nouveau projet :

npm install -g @angular/cli@19
ng new signals-dashboard --style=css --ssr=false --standalone
cd signals-dashboard

Ouvrez angular.json et vérifiez que "strict": true figure bien dans les options TypeScript. Angular 19 active le mode strict par défaut, ce qui se marie parfaitement avec les Signals.

Activez maintenant le mode zoneless. Modifiez src/main.ts :

import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
 
bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideHttpClient(),
  ],
}).catch((err) => console.error(err));

Supprimez zone.js de polyfills dans angular.json pour gagner environ 40 Ko sur le bundle.

Étape 2 : comprendre les trois primitives Signal

Les Signals sont des valeurs réactives qui notifient leurs consommateurs de tout changement. Angular expose trois primitives :

import { signal, computed, effect } from '@angular/core';
 
const count = signal(0);
const double = computed(() => count() * 2);
 
effect(() => {
  console.log(`count vaut ${count()}, double vaut ${double()}`);
});
 
count.set(5);
count.update((n) => n + 1);

Trois points à retenir :

  1. Lisez avec count(), comme un appel de fonction.
  2. Écrivez avec .set() ou .update() sur un signal modifiable.
  3. Dérivez avec computed(), qui mémoïse automatiquement.

effect() s'exécute chaque fois qu'un signal lu à l'intérieur change. Les effets sont nettoyés à la destruction du composant propriétaire — aucune désinscription manuelle.

Étape 3 : construire un store basé sur Signals

Créez src/app/stores/preferences.store.ts :

import { Injectable, signal, computed, effect } from '@angular/core';
 
export type SortBy = 'stars' | 'updated' | 'name';
 
@Injectable({ providedIn: 'root' })
export class PreferencesStore {
  readonly query = signal('');
  readonly sortBy = signal<SortBy>('stars');
  readonly minStars = signal(0);
 
  readonly activeFiltersCount = computed(() => {
    let count = 0;
    if (this.query().length > 0) count++;
    if (this.minStars() > 0) count++;
    return count;
  });
 
  constructor() {
    const saved = localStorage.getItem('prefs');
    if (saved) {
      const parsed = JSON.parse(saved);
      this.query.set(parsed.query ?? '');
      this.sortBy.set(parsed.sortBy ?? 'stars');
      this.minStars.set(parsed.minStars ?? 0);
    }
 
    effect(() => {
      const snapshot = {
        query: this.query(),
        sortBy: this.sortBy(),
        minStars: this.minStars(),
      };
      localStorage.setItem('prefs', JSON.stringify(snapshot));
    });
  }
 
  reset(): void {
    this.query.set('');
    this.sortBy.set('stars');
    this.minStars.set(0);
  }
}

Voici un store réactif complet en une trentaine de lignes. Pas de Redux, pas de boilerplate NgRx, pas de pipelines Observable. L'effect() synchronise silencieusement chaque changement vers localStorage.

Étape 4 : récupérer les données avec la Resource API

La Resource API est la réponse d'Angular 19 aux états de chargement et d'erreur. Créez src/app/services/repos.service.ts :

import { Injectable, computed, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { PreferencesStore } from '../stores/preferences.store';
 
export interface Repo {
  id: number;
  name: string;
  full_name: string;
  description: string | null;
  stargazers_count: number;
  updated_at: string;
  html_url: string;
}
 
@Injectable({ providedIn: 'root' })
export class ReposService {
  private readonly prefs = inject(PreferencesStore);
 
  readonly reposResource = httpResource<Repo[]>(() => {
    const q = this.prefs.query().trim();
    if (!q) return undefined;
    return `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}`;
  });
 
  readonly filteredRepos = computed(() => {
    const repos = this.reposResource.value() ?? [];
    const minStars = this.prefs.minStars();
    const sortBy = this.prefs.sortBy();
 
    const filtered = repos.filter((r) => r.stargazers_count >= minStars);
 
    return [...filtered].sort((a, b) => {
      if (sortBy === 'stars') return b.stargazers_count - a.stargazers_count;
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return b.updated_at.localeCompare(a.updated_at);
    });
  });
}

httpResource prend une fonction renvoyant une URL. Quand cette fonction est réévaluée parce qu'une de ses dépendances réactives a changé, Angular récupère automatiquement les nouvelles données et expose les signals value(), status() et error().

Retourner undefined depuis la fabrique d'URL met la requête en pause, une façon élégante d'éviter de fetcher quand l'utilisateur n'a rien tapé.

Étape 5 : contrôle de flux moderne avec @if et @for

Les directives structurelles historiques *ngIf et *ngFor sont remplacées par une syntaxe en blocs qui se parse plus vite et se lit plus naturellement. Créez src/app/components/repo-list.component.ts :

import { Component, inject } from '@angular/core';
import { ReposService } from '../services/repos.service';
 
@Component({
  selector: 'app-repo-list',
  standalone: true,
  template: `
    @if (repos.reposResource.status() === 'loading') {
      <p class="loading">Chargement des dépôts...</p>
    } @else if (repos.reposResource.error()) {
      <p class="error">Échec du chargement : {{ repos.reposResource.error()?.message }}</p>
    } @else {
      @for (repo of repos.filteredRepos(); track repo.id) {
        <article class="repo-card">
          <h3>
            <a [href]="repo.html_url" target="_blank">{{ repo.full_name }}</a>
          </h3>
          @if (repo.description) {
            <p>{{ repo.description }}</p>
          }
          <div class="meta">
            <span>&#9733; {{ repo.stargazers_count }}</span>
            <span>Mis à jour le {{ repo.updated_at | date: 'mediumDate' }}</span>
          </div>
        </article>
      } @empty {
        <p class="empty">Aucun dépôt ne correspond à vos filtres.</p>
      }
    }
  `,
  styleUrl: './repo-list.component.css',
})
export class RepoListComponent {
  readonly repos = inject(ReposService);
}

Les améliorations clés par rapport à l'Angular classique :

  • Le bloc @empty gère proprement les collections vides
  • track est obligatoire, ce qui évite le bug classique du trackBy oublié
  • Le compilateur détecte les erreurs de syntaxe au build plutôt qu'à l'exécution

Étape 6 : construire une barre de recherche basée sur Signals

Les formulaires basés sur Signals sont stables dans Angular 19. Créez src/app/components/search-bar.component.ts :

import { Component, inject } from '@angular/core';
import { PreferencesStore } from '../stores/preferences.store';
 
@Component({
  selector: 'app-search-bar',
  standalone: true,
  template: `
    <div class="search-bar">
      <input
        type="search"
        placeholder="Rechercher des dépôts GitHub..."
        [value]="prefs.query()"
        (input)="onQueryChange($event)"
      />
      <select
        [value]="prefs.sortBy()"
        (change)="onSortChange($event)"
      >
        <option value="stars">Plus d'étoiles</option>
        <option value="updated">Récemment mis à jour</option>
        <option value="name">Nom A à Z</option>
      </select>
      <input
        type="number"
        min="0"
        placeholder="Étoiles min"
        [value]="prefs.minStars()"
        (input)="onMinStarsChange($event)"
      />
      @if (prefs.activeFiltersCount() > 0) {
        <button (click)="prefs.reset()">
          Réinitialiser ({{ prefs.activeFiltersCount() }})
        </button>
      }
    </div>
  `,
})
export class SearchBarComponent {
  readonly prefs = inject(PreferencesStore);
 
  onQueryChange(event: Event): void {
    const value = (event.target as HTMLInputElement).value;
    this.prefs.query.set(value);
  }
 
  onSortChange(event: Event): void {
    const value = (event.target as HTMLSelectElement).value as 'stars' | 'updated' | 'name';
    this.prefs.sortBy.set(value);
  }
 
  onMinStarsChange(event: Event): void {
    const value = Number((event.target as HTMLInputElement).value);
    this.prefs.minStars.set(Number.isFinite(value) ? value : 0);
  }
}

Quand l'utilisateur tape, le signal query se met à jour. La fabrique de httpResource est réévaluée et refait le fetch. Le computed filteredRepos recalcule et le template se re-rend, le tout sans appeler markForCheck ni injecter ChangeDetectorRef.

Étape 7 : connecter tout dans le composant racine

Modifiez src/app/app.component.ts :

import { Component } from '@angular/core';
import { SearchBarComponent } from './components/search-bar.component';
import { RepoListComponent } from './components/repo-list.component';
 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [SearchBarComponent, RepoListComponent],
  template: `
    <header>
      <h1>Tableau de bord Angular 19 Signals</h1>
    </header>
    <main>
      <app-search-bar />
      <app-repo-list />
    </main>
  `,
})
export class AppComponent {}

Lancez l'application avec ng serve puis ouvrez http://localhost:4200. Tapez un terme, ajustez le filtre d'étoiles, et observez la liste se mettre à jour instantanément sans un seul subscribe manuel.

Étape 8 : débouncer les entrées utilisateur avec l'interop RxJS

Il arrive qu'on veuille l'ergonomie des Observables, par exemple pour débouncer un champ de recherche. Angular expose toSignal et toObservable pour une interop fluide.

Mettez à jour ReposService pour débouncer la requête :

import { Injectable, computed, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';
import { PreferencesStore } from '../stores/preferences.store';
 
@Injectable({ providedIn: 'root' })
export class ReposService {
  private readonly prefs = inject(PreferencesStore);
 
  private readonly debouncedQuery = toSignal(
    toObservable(this.prefs.query).pipe(
      debounceTime(300),
      distinctUntilChanged(),
    ),
    { initialValue: '' }
  );
 
  readonly reposResource = httpResource<{ items: Repo[] }>(() => {
    const q = this.debouncedQuery().trim();
    if (!q) return undefined;
    return `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}`;
  });
}

C'est le pattern recommandé par l'équipe Angular pour les bases de code hybrides : utiliser les Signals pour l'état, n'utiliser RxJS que lorsque vous avez besoin d'opérateurs comme debounceTime, switchMap ou retryWhen.

Étape 9 : tester du code basé sur Signals

Les Signals sont des fonctions pures, donc trivialement testables. Créez preferences.store.spec.ts :

import { TestBed } from '@angular/core/testing';
import { PreferencesStore } from './preferences.store';
 
describe('PreferencesStore', () => {
  let store: PreferencesStore;
 
  beforeEach(() => {
    localStorage.clear();
    TestBed.configureTestingModule({});
    store = TestBed.inject(PreferencesStore);
  });
 
  it('démarre avec les valeurs par défaut', () => {
    expect(store.query()).toBe('');
    expect(store.sortBy()).toBe('stars');
    expect(store.activeFiltersCount()).toBe(0);
  });
 
  it('calcule le nombre de filtres actifs', () => {
    store.query.set('angular');
    store.minStars.set(100);
    expect(store.activeFiltersCount()).toBe(2);
  });
 
  it('réinitialise toutes les préférences', () => {
    store.query.set('test');
    store.reset();
    expect(store.query()).toBe('');
  });
});

Pas de fakeAsync, pas de TestScheduler, pas de callback done. On pose un signal, on lit un autre, on assert. Cette simplicité est le meilleur argument pour adopter les Signals.

Étape 10 : builder et déployer

Produisez un build de production optimisé :

ng build --configuration production

Le dossier dist/signals-dashboard/browser contient des assets statiques prêts pour n'importe quel CDN. Sur Vercel ou Netlify, la sortie par défaut fonctionne telle quelle. Sur un VPS classique, servez index.html avec un fallback pour gérer le routage côté client.

Vérifiez la taille du bundle :

npx source-map-explorer dist/signals-dashboard/browser/main-*.js

Une application Angular 19 zoneless atteint environ 110 Ko gzippés pour ce dashboard, comparable à un projet React + React Query de complexité similaire.

Tester votre implémentation

Passez cette checklist :

  1. L'application se charge sans erreur dans la console
  2. Taper dans la zone de recherche déclenche le fetch après 300 ms de debounce
  3. La liste déroulante de tri réordonne les résultats instantanément
  4. Définir un minimum d'étoiles masque les dépôts en dessous du seuil
  5. Rafraîchir la page restaure la dernière requête, le tri et le filtre
  6. L'état de chargement s'affiche pour les requêtes lentes
  7. Couper le réseau affiche une erreur visible, pas un échec silencieux

Dépannage

NG0203: inject() must be called from an injection context Vous appelez inject() hors d'un constructeur, d'un initialiseur de champ ou d'une factory. Déplacez-le dans le constructeur ou en initialiseur de champ.

Error: NG02200: Cannot find a differ supporting object Un bloc @for sans track. Ajoutez track item.id ou toute clé unique qui identifie chaque élément.

La Resource refait un fetch à chaque rendu Votre fabrique d'URL dépend d'une valeur qui change à chaque cycle de détection. Encapsulez cette dépendance dans un computed() pour la stabiliser.

Le hot reload se casse après l'ajout de Signals Les anciennes versions de l'Angular CLI cachent agressivement. Exécutez rm -rf .angular puis relancez ng serve.

Pour aller plus loin

  • Ajoutez le chargement de données au niveau route via la Resource API dans des guards resolve
  • Activez l'hydratation incrémentale pour les pages SSR avec @defer
  • Explorez NgRx SignalStore, qui construit un store complet au-dessus des Signals
  • Intégrez TanStack Query pour Angular pour du cache avancé et du refetch en arrière-plan
  • Lisez le guide officiel des Signals Angular pour les cas limites autour des effects

Vous apprécierez peut-être aussi ces tutoriels connexes :

Conclusion

Angular 19 redevient un véritable framework moderne. Les Signals offrent une réactivité fine sans la surcharge de Zone.js, la Resource API réduit le boilerplate asynchrone à presque rien, et les composants standalone vous laissent enfin écrire de l'Angular sans apercevoir un seul module.

Si vous avez évalué Angular il y a cinq ans et décroché, cette version mérite un second regard. Les équipes avec une grosse base Angular obtiennent un chemin de migration incrémental clair. Les équipes qui démarrent obtiennent un framework avec piles incluses, TypeScript de première classe, injection de dépendances native et un rythme de releases durable.

Le tableau de bord que vous venez de construire est le squelette de toute application Angular 19 sérieuse : état réactif, récupération typée des données, entrée débouncée et tests propres. Clonez-le, étendez-le et livrez votre prochain projet à la façon moderne d'Angular.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créer des applications IA avec Google Gemini API et TypeScript.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·