🚀 Angular 20 is Here – Deep Dive into toSignal() and Signal-Based APIs
vetriselvan Panneerselvam

vetriselvan Panneerselvam @vetriselvan_11

About: 👋 Hi, I'm Vetriselvan. A passionate front-end developer who loves turning ideas into clean, functional, and user-friendly interfaces. I enjoy writing code and sharing what I learn along the way.

Joined:
Jun 10, 2025

🚀 Angular 20 is Here – Deep Dive into toSignal() and Signal-Based APIs

Publish Date: Jun 10
1 2

Angular 20 has officially landed! 🎉 As expected, one of the most exciting updates is the continued evolution of Signal-based APIs, a huge step forward in Angular's reactive programming model.

In this post, we'll explore one such API — toSignal() — and learn how it helps bridge the gap between Observables and Signals.

🔁 What is toSignal()?

The toSignal() function allows you to convert an Observable stream into a Signal, enabling integration with Angular’s fine-grained reactive system.

This makes it easier to work with API responses, state changes, and other reactive data sources in a way that's more performant and declarative.

toSignal<T>(source: Observable<T>, options?: {
  initialValue?: unknown;
  requireSync?: boolean;
  manualCleanup?: boolean;
  injector?: Injector;
  equal?: ValueEqualityFn<T>;
}): Signal<T>
Enter fullscreen mode Exit fullscreen mode

✅ Basic Example: Converting an HTTP Observable to a Signal

Here’s a quick demo of how toSignal() can be used to fetch data from a JSON file and convert the response into a Signal.

import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, effect } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';

@Component({
  selector: 'app-angular-api',
  templateUrl: './angular-api.html',
  styleUrl: './angular-api.scss',
})
export class AngularApi implements OnInit {
  private http = inject(HttpClient);
  URL = './json/country.json';

  countryList = toSignal(
    this.http.get<any>(this.URL).pipe(map((res) => res))
  );

  constructor() { 
    effect( 
      (onCleanup) => {
        console.log('country value :', this.countryList());
        onCleanup(() => {
          console.log('clean up callback triggered');
        });
      }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we use toSignal() to convert the HTTP observable into a reactive signal, which automatically updates when the observable emits.

🧼 Note:
The onCleanup() inside the effect() function gets triggered:

  • When the injection context is destroyed (e.g., component, service gets destroyed)
  • Or when the signal dependency changes and the effect re-runs.

Image description

⚠️ requireSync Option

When requireSync: true is used, Angular expects the observable to emit immediately upon subscription.

✅ Works with Synchronous Observables

countryList = toSignal(
  of([{ name: { common: 'Aruba' }, region: 'Americas', flag: '🇦🇼' }]),
  { requireSync: true }
);
Enter fullscreen mode Exit fullscreen mode

Since of() emits synchronously, this works just fine.

Certainly! Here's how you can rewrite that line as a note for a Dev.to blog post in a clean and developer-friendly format:

📝 Note:
When using requireSync: true, there's no need to provide an initialValue, because the observable is expected to emit synchronously.
As a result, the returned signal will not include undefined in its type.


Let me know if you'd like to bundle this with other requireSync tips or use it inline with an example.

❌ Fails with Asynchronous Observables

countryList = toSignal(
  of([{ name: { common: 'Aruba' } }]).pipe(delay(500)),
  { requireSync: true }
);
Enter fullscreen mode Exit fullscreen mode

💥 Adding a delay turns the stream asynchronous, and Angular throws an error:

ERROR RuntimeError: NG0601: `toSignal()` called with `requireSync` but `Observable` did not emit synchronously.

🧠 Quick Tip:
timer(), interval(), and HTTP calls are asynchronous,
while of(), BehaviorSubject, and ReplaySubject are synchronous.

🛠️ initialValue Option

You can provide a default value for your signal using the initialValue option. This is useful when your observable takes time to emit.

timer$ = timer(1000);
timerLog = toSignal(timer$, {
  initialValue: 0
});
Enter fullscreen mode Exit fullscreen mode

📝 Note:
The initialValue provided to toSignal will be used until the observable emits its first value. This is especially useful when dealing with asynchronous streams that don’t emit immediately.

📝 The signal will start with the value 0 and update when timer$ emits.

🔁 Custom Equality with equal

The equal function allows you to define custom logic to determine whether the signal should update.

timer$ = timer(1000);
timerLog = toSignal(timer$, {
  initialValue: 0,
  equal: (prev:number, curr:number) => {
    console.log(prev, curr);
    return curr === 5;
  }
});
Enter fullscreen mode Exit fullscreen mode

✅ If the function returns true, the signal won’t update.
❌ If it returns false, it will.

💉 Using injector Outside the Injection Context

Trying to use toSignal() outside of the constructor or runInInjectionContext() will result in

ERROR RuntimeError: NG0203: toSignal() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`
Enter fullscreen mode Exit fullscreen mode

But you can fix this by explicitly passing the injector:

this.countryList = toSignal(
  this.http.get<any>(this.URL).pipe(map((res) => res)),
  { injector: this._injector }
);
Enter fullscreen mode Exit fullscreen mode

🧹 Controlling Cleanup with manualCleanup

By default, toSignal() will automatically unsubscribe when the component is destroyed. But with manualCleanup: true, you control the cleanup.

this.countryList = toSignal(
  this.http.get<any>(this.URL).pipe(
    takeUntil(this._destroy$),
    map((res: any) => res)
  ),
  { manualCleanup: true }
);
Enter fullscreen mode Exit fullscreen mode

📌 Final Thoughts

Angular 20's signal APIs—especially toSignal()—are making state management and reactive data flow much easier and more powerful.

Whether you're working with async HTTP calls, state streams, or UI interactions, understanding how to leverage toSignal() and its options can help you build faster, cleaner, and more maintainable apps.

Let me know if you'd like a follow-up post on computed, effect, or other new Angular 20 features!

✍️ Author: Vetriselvan

👨‍💻 Frontend Developer | Code Lover | Exploring Angular’s future

Comments 2 total

Add comment