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

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
toSignalettoObservable - 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
ngIfoungFor, grâce à la nouvelle syntaxe de contrôle@ifet@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-dashboardOuvrez 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 :
- Lisez avec
count(), comme un appel de fonction. - Écrivez avec
.set()ou.update()sur un signal modifiable. - 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>★ {{ 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
@emptygère proprement les collections vides trackest obligatoire, ce qui évite le bug classique dutrackByoublié- 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 productionLe 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-*.jsUne 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 :
- L'application se charge sans erreur dans la console
- Taper dans la zone de recherche déclenche le fetch après 300 ms de debounce
- La liste déroulante de tri réordonne les résultats instantanément
- Définir un minimum d'étoiles masque les dépôts en dessous du seuil
- Rafraîchir la page restaure la dernière requête, le tri et le filtre
- L'état de chargement s'affiche pour les requêtes lentes
- 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 :
- Vite 6, React, TypeScript pour une app web moderne
- Biome, remplacer ESLint et Prettier par un seul outil
- TanStack Query v5 pour la récupération de données dans Next.js
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.
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

Vite 6 + React + TypeScript : créer une application web moderne de A à Z en 2026
Maîtrisez Vite 6 avec React et TypeScript — de la création du projet au déploiement. Ce guide complet couvre l'Environment API, le HMR ultra-rapide, le bundling optimisé, les tests avec Vitest et les meilleures pratiques de production.

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.

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.