Angular 19 Signals and Resource API: Build Reactive Web Apps in 2026

Noqta Team
By Noqta Team ·

Loading the Text to Speech Audio Player...

Angular 19 changes everything you thought you knew about the framework. Signals replace RxJS for state, the Resource API turns async data into first-class reactive primitives, and zoneless change detection delivers React-level performance. In this tutorial you will build a complete reactive dashboard and master the modern Angular stack from day one.

What You Will Learn

By the end of this tutorial, you will be able to:

  • Create standalone components without NgModules
  • Use Signals (signal, computed, effect) for reactive state
  • Fetch remote data with the new Resource API and httpResource
  • Run Angular in zoneless mode for maximum performance
  • Build signal-based forms with type safety
  • Bridge RxJS observables and Signals with toSignal and toObservable
  • Deploy a production-ready Angular app

Prerequisites

Before starting, ensure you have:

  • Node.js 20+ (Angular 19 requires Node 20 or later)
  • Basic TypeScript knowledge (interfaces, generics, decorators)
  • Familiarity with HTML and CSS
  • A code editor, VS Code with the Angular Language Service extension recommended
  • Angular CLI experience is helpful but not mandatory

Why Angular 19 Matters

For years, Angular felt heavy compared to React or Vue. NgModules required boilerplate, Zone.js patched globals to trigger change detection, and RxJS was the only idiomatic way to handle async. Angular 19 fixes all of this:

  • Standalone by default — no NgModules, cleaner imports
  • Signals — fine-grained reactivity without Zone.js overhead
  • Zoneless change detection — smaller bundles, faster rendering
  • Resource API — reactive data fetching with loading and error states built in
  • Incremental hydration — server-rendered apps that hydrate on demand

The result is an Angular that feels modern, lightweight, and a genuine alternative to React Server Components.

What You Will Build

A real-time Signals Dashboard that displays GitHub repository statistics. The app will:

  • Fetch repos from the GitHub API using httpResource
  • Filter and sort them reactively with Signals
  • Persist user preferences with a Signal-based store
  • Update the UI instantly with zero ngIf or ngFor boilerplate, using the new @if and @for control flow

Step 1: Create the Angular 19 Project

Install the latest Angular CLI and generate a new project:

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

Open angular.json and confirm "strict": true under the TypeScript options. Angular 19 ships with strict mode on by default, which pairs nicely with Signals.

Now enable zoneless change detection. Edit 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));

Delete zone.js from polyfills in angular.json to shave around 40 KB from the bundle.

Step 2: Understand the Three Core Signal Primitives

Signals are reactive values that notify their consumers when they change. Angular exposes three primitives:

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

Three things to notice:

  1. Read with count(), like calling a function.
  2. Write with .set() or .update() on a writable signal.
  3. Derive with computed(), which memoizes automatically.

effect() runs whenever any read signal changes. Effects are cleaned up when their owning component is destroyed — no manual unsubscribe.

Step 3: Build a Signal-Based Store

Create 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);
  }
}

This is a complete reactive store in roughly 30 lines. No Redux, no NgRx boilerplate, no Observable pipelines. The effect() call silently syncs every change to localStorage.

Step 4: Fetch Data with the Resource API

The Resource API is Angular 19's answer to loading and error states. Create 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 takes a function returning a URL. When the function re-evaluates because one of its reactive dependencies changed, Angular automatically fetches the new data and exposes value(), status(), and error() signals.

Returning undefined from the URL factory pauses the request, a clean way to avoid fetching when the user has not typed anything yet.

Step 5: Modern Control Flow with @if and @for

The legacy structural directives *ngIf and *ngFor are replaced with a block syntax that parses faster and reads more naturally. Create 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">Loading repositories...</p>
    } @else if (repos.reposResource.error()) {
      <p class="error">Failed to load: {{ 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>Updated {{ repo.updated_at | date: 'mediumDate' }}</span>
          </div>
        </article>
      } @empty {
        <p class="empty">No repositories match your filters.</p>
      }
    }
  `,
  styleUrl: './repo-list.component.css',
})
export class RepoListComponent {
  readonly repos = inject(ReposService);
}

Key improvements over legacy Angular:

  • @empty block handles empty collections cleanly
  • track is required, which prevents the common trackBy bug
  • The compiler catches syntax errors at build time rather than runtime

Signal-based forms are stable in Angular 19. Create 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="Search GitHub repos..."
        [value]="prefs.query()"
        (input)="onQueryChange($event)"
      />
      <select
        [value]="prefs.sortBy()"
        (change)="onSortChange($event)"
      >
        <option value="stars">Most stars</option>
        <option value="updated">Recently updated</option>
        <option value="name">Name A to Z</option>
      </select>
      <input
        type="number"
        min="0"
        placeholder="Min stars"
        [value]="prefs.minStars()"
        (input)="onMinStarsChange($event)"
      />
      @if (prefs.activeFiltersCount() > 0) {
        <button (click)="prefs.reset()">
          Reset ({{ 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);
  }
}

When the user types, the query signal updates. The httpResource factory re-evaluates and refetches. The filteredRepos computed signal recalculates and the template re-renders, all without calling markForCheck or injecting ChangeDetectorRef.

Step 7: Wire Everything into the Root Component

Edit 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>Angular 19 Signals Dashboard</h1>
    </header>
    <main>
      <app-search-bar />
      <app-repo-list />
    </main>
  `,
})
export class AppComponent {}

Run the app with ng serve and open http://localhost:4200. Type a search term, adjust the star filter, and watch the list update instantly without a single manual subscription.

Step 8: Debounce User Input with RxJS Interop

Sometimes you still want Observable ergonomics, for example to debounce a search input. Angular exposes toSignal and toObservable for seamless interop.

Update ReposService to debounce the query:

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)}`;
  });
}

This is the pattern the Angular team recommends for hybrid codebases: use Signals for state, reach for RxJS only when you need operators like debounceTime, switchMap, or retryWhen.

Step 9: Test Signal-Based Code

Signals are pure functions, which makes them trivially testable. Create 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('starts with default values', () => {
    expect(store.query()).toBe('');
    expect(store.sortBy()).toBe('stars');
    expect(store.activeFiltersCount()).toBe(0);
  });
 
  it('computes active filter count', () => {
    store.query.set('angular');
    store.minStars.set(100);
    expect(store.activeFiltersCount()).toBe(2);
  });
 
  it('resets all preferences', () => {
    store.query.set('test');
    store.reset();
    expect(store.query()).toBe('');
  });
});

No fakeAsync, no TestScheduler, no done callbacks. Set a signal, read another, assert. This simplicity is the strongest argument for moving to Signals.

Step 10: Build and Deploy

Produce an optimized build:

ng build --configuration production

The dist/signals-dashboard/browser folder contains static assets ready for any CDN. For Vercel or Netlify, the default output works out of the box. For a traditional VPS, serve index.html with a fallback to handle client-side routing.

Check the bundle size:

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

A zoneless Angular 19 app lands around 110 KB gzipped for this dashboard, comparable to a React + React Query project of similar complexity.

Testing Your Implementation

Walk through this checklist:

  1. The app loads without errors in the console
  2. Typing in the search box fetches data after a 300 ms debounce
  3. The sort dropdown reorders results instantly
  4. Setting a minimum star filter hides repos under the threshold
  5. Refreshing the page restores your last query, sort, and filter
  6. The loading state shows for slow requests
  7. Disabling your network throws a visible error, not a silent failure

Troubleshooting

NG0203: inject() must be called from an injection context You are calling inject() outside a constructor, field initializer, or factory function. Move it into the constructor or use a field initializer.

Error: NG02200: Cannot find a differ supporting object A @for block is missing track. Add track item.id or whatever unique key identifies each item.

Resource keeps refetching on every render Your URL factory depends on a value that changes on every change detection cycle. Wrap the offending dependency in a computed() to stabilize it.

Hot reload breaks after adding Signals Older versions of the Angular CLI cache aggressively. Run rm -rf .angular and restart ng serve.

Next Steps

  • Add route-level data loading with the Resource API inside resolve guards
  • Enable incremental hydration for server-rendered pages with @defer
  • Explore NgRx SignalStore, which builds a feature-complete store on top of Signals
  • Integrate TanStack Query for Angular for advanced caching and background refetching
  • Read the official Angular Signals guide for edge cases around effects

You may also enjoy these related tutorials:

Conclusion

Angular 19 is a legitimate modern framework again. Signals give you fine-grained reactivity without the overhead of Zone.js, the Resource API reduces async boilerplate to almost nothing, and standalone components finally let you write Angular without a module in sight.

If you evaluated Angular five years ago and walked away, this is the release that deserves a second look. Teams with a large Angular codebase get a clear incremental migration path. Teams starting fresh get a batteries-included framework with first-class TypeScript, dependency injection, and a sustainable long-term release cadence.

The dashboard you just built is the skeleton of any serious Angular 19 app: reactive state, typed data fetching, debounced input, and clean testing. Clone it, extend it, and ship your next project the modern Angular way.


Want to read more tutorials? Check out our latest tutorial on Tauri 2 + React: Build a Cross-Platform Desktop App with Rust.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles