Activating Angular's
Reactive Core

Ankita Sood

(uh nk - ee t ah   s oo d)
SoodAnkita
ankitasood.bsky.social
WebVibesOnly
GuacamoleAnkita

Reactivity

Reactivity

Reactivity

Reactivity can be broadly defined as the automatic update of the UI due to a change in the application's state.

There are as many definitions of reactive programming
as there are
reactive programmers.

Reactivity
in
Angular

Coarse Grained Reactivity

  • Re-runs change detection for entire component tree when any async event occurs.

  • Uses Zone.js to monkey-patch browser APIs and trigger global change detection.

  • Components re-evaluate even if their data hasn't changed.

Fine Grained Reactivity

  • Updates only the specific DOM elements that depend on changed signals.

  • Skips components whose signals haven't changed.

  • Enables zoneless Angular applications.

Reactivity in Angular

Evolved from being primarily
RxJS-based Observables to Signals.

Observables

Used for event handling & asynchronous programming to manage multiple values emitted over time.

Observables

The most common uses:

  • Handling HTTP requests.

  • Routing.

  • Forms modules.

  • Event handling - Dom events, custom component events, web socket events.

Signals

 Lightweight wrapper around a value that notifies interested consumers when that value changes.

  • Used to watch and react to data changes.
     
  • Value based reactivity - synchronous.
     
  • Built specifically for UI reactivity.
     
  • No manual subscription necessary.

Signals

  •  Used to watch and react to events over time.
     
  • Stream-based data flow - asynchronous.
     
  • Great for events, HTTP, & complex data flows.
  • Needs to be subscribed and unsubscribed.

Observables

Observables Vs Signals

class ItemCount implements OnDestroy {
 count = new BehaviorSubject(0);
 count$ = this.count.asObservable();

 increment():void {
  const curr = this.count.value;
  this.count.next(curr + 1);
}
 ngOnDestroy(): void {
  this.count.complete();
 }
}
class ItemCount {
  count = signal(0);
  
  increment(): void {
    this.count.update(curr => curr + 1);
  }
}

Signals

Writable Signals

A Signal with a value that can be mutated.

Read-only Signals

Cannot directly assign values to it.

Signal

const count = signal(0);  // Create

count.set(5);             // Set directly

count.update(c => c + 1); // Update based on previous

Computed Signals

const useDiscount = signal(false);
const price = signal(100);
const discount = signal(0.1);

const finalPrice = computed(() => {
  return useDiscount() ? price() * (1 - discount()) : price();
});

Linked Signals

const items = signal(['Apple', 'Banana', 'Cherry']);

// linked signal: always shows the first item
selected = linkedSignal(() => items()[0]);
console.log(selected());  // → 'Apple'

items.set(['Orange', 'Pear']);
console.log(selected());  // → 'Orange'

Linked Signals

items = input<string[]>([]);

// linked signal: default to first item, null if no input
selected = linkedSignal({
  source: ()=> this.items(),
  computation: (source, previous) => {
    return source.find(i => previous?.value === i) || null;
  }
});

isSelected (item: string) {
  return this.selected() === item;
}
 @for (item of items(); track $index) {
   <a (click)="selected.set(item)">{{item}}</a>
  }
  <span>{{selected() || "No item selected."}}</span>

Linked Signals

selectedItem = signal<Item | null>(null);
price = computed(()=> this.selectedItem()?.price ?? 0);

// quantity depends on selected Item
quantity = linkedSignal({
            source: this.selectedItem,
            computation: () => 1
		   });
total = computed(()=> this.quantity() * this.price());

onQuantityChanged(q: number) {
  this.quantity.set(q);
}
  • Reset a signal when one or more signals change.
     
  • Access previous value of a signal.
     
  • Writeable result.

COMPUTED

  • Derive a value from one or more signals.
     
  • Re-compute when those signals change.
     

  • Read-only result.

LINKED SIGNALS

Usage Guidelines

Effects

Operations that run when signal values change.

Effects

export class EffectiveCounter {
  readonly count = signal(0);
  constructor() {
    // Register a new effect.
    effect(() => {
      console.log(`The count is: ${this.count()}`);
    });
  }
}
  • Execute at least once.
  • Track signal dependencies dynamically.
  • Always execute asynchronously.

Effects

Pass an Injector to create outside the constructor.

export class EffectiveCounter {
  readonly count = signal(0);
  private injector = inject(Injector);
  initializeLogging(): void {
    effect(() => {
      console.log(`The count is: ${this.count()}`);
    }, {injector: this.injector});
  }
}

Automatically destroyed when its enclosing context is destroyed.

  • Performing custom rendering to a canvas, charting library, or other third party UI library.
     

  • Logging.
     

  • Syncing data with window.localStorage.
     

  • Adding custom DOM behavior.

When to Use

When to Avoid

Don't use effects for state propagation!

  •  Can result in ExpressionChangedAfterItHasBeenChecked errors.
     
  • Infinite circular updates.
     
  • Unnecessary change detection cycles.
effect(() => {
  console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`);
});

Prevent a signal read from being tracked by calling its getter with untracked.

Effects Decision Tree

Deborah Kurata's Effects decision tree: https://youtu.be/XWz8pxQWD8c?si=Xrgy7Is9ESCvoZUE 

Component Specific Signals

Writable Signals

Model Inputs

Non-Writable Signals

InputSignal
View Queries (viewChild, viewChildren)
Content Queries (contentChild, contentChildren)

Model Inputs

export class DateComponent {
 @Input() value = new Date();
 @Output() valueChange = new EventEmitter();
 value = model(new Date());
}

Model Inputs

@Component({
  selector: 'app-settings-page',
  template: `<h2>Dark Mode Setting</h2>
    <app-toggle-switch [(checked)]="darkModeEnabled" />
    <p>Dark Mode is: {{ darkModeEnabled() ? 'ON' : 'OFF' }}</p>`,
})
export class SettingsPage {
  darkModeEnabled = signal(false);
}

@Component({
  selector: 'app-toggle-switch',
  // template: A switch UI that calls this.checked.set(!this.checked()) on click
})
export class ToggleSwitch {
  checked = model(false); 

  toggle(): void {
    this.checked.update(isCurrentlyChecked => !isCurrentlyChecked);
  }
}

Input Signals

export class ListItem {
  @Input("required")title: string = “”;
  title = input.required<string>();
  @Input({transform: (value: string) => value.toUpperCase()}) descr: string = "Default description"
  descr = input('Default description',{transform: (value: string) => value.toUpperCase()});
}

View Queries

export class SearchDialog {
  isOpen = false;
  @ViewChild('search') searchInput: ElementRef<HTMLElement> | undefined;
  searchInput = viewChild.required<ElementRef<HTMLElement>>('search');
  
  // some additional logic goes here that updates value of isOpen
  
  ngAfterViewInit(): void {
    if (isOpen) {
      this.searchInput.nativeElement.focus();
      this.searchInput().nativeElement.focus();
    }
  }
}

Content Queries

@Component({
  selector: 'custom-toggle',
})
export class CustomToggle {
  text: string;
}
@Component({
  selector: 'custom-expando',
})
export class CustomExpando {
  toggle = contentChild(CustomToggle);
  toggleText = computed(() => this.toggle()?.text);
}
@Component({ 
  template: `
    <custom-expando>
      <custom-toggle>Show</custom-toggle>
    </custom-expando>
  `
})
export class UserProfile { }
@Component({
  selector: 'custom-toggle',
})
export class CustomToggle {
  text: string;
}
@Component({
  selector: 'custom-expando',
})
export class CustomExpando {
  @ContentChild(CustomToggle) toggle: CustomToggle;
  ngAfterContentInit() {
    console.log(this.toggle.text);
  }
}
@Component({
  template: `
    <custom-expando>
      <custom-toggle>Show</custom-toggle>
    </custom-expando>
  `
})
export class UserProfile { }

Asynchronous Reactivity

  • resource(): Lower level, works with promises.

  • httpResource(): Works with a url string.

  • rxResource: Works with Observables.

Experimental APIs

Resource

Streamline asynchronous data fetching with built-in reactivity.

  • Includes built-in status tracking (loading, error, success).

  • Similar behavior to switchMap built-in to handle consecutive requests (AbortSignal).

  • Easy to set default value & trigger reload.

  • Doesn't rely on zone.js.

Resource

const userId: Signal<string> = getUserId();
const userResource = resource({
  params: () => ({id: userId()}),
  loader: ({params}) => fetchUser(params),
  defaultValue: ''
});

const firstName = computed(() => {
  if (userResource.hasValue()) {
    return userResource.value().firstName;
  }
  // fallback in case the resource value is `undefined` or if the resource is in error state
  return undefined;
});

Resource

const userId: Signal<string> = getUserId();
const userResource = resource({
 params: () => ({id: userId()}),
 loader: ({params, abortSignal}): Promise<User> => {
  // fetch cancels any outstanding HTTP requests when the given `AbortSignal`
  // indicates that the request has been aborted.
  return fetch(`users/${params.id}`,
         {signal: abortSignal});
 },
});
// programatically trigger a resource's loader
userResource.reload();

httpResource

  • Wrapper around HttpClient.

  • Initiates the request eagerly.

  • Exposes the request status and response values as a WriteableResource.

  • Only for retrive operations only i.e. get().

Facilitates http requests.

httpResource

@if(user.hasValue()) {
  <user-details [user]="user.value()">
} @else if (user.error()) {
  <div>Could not load user information</div>
} @else if (user.isLoading()) {
  <div>Loading user info...</div>
}
userId = input.required<string>();
user = httpResource(() => `/api/user/${userId()}`); // A reactive function as argument

The signals of the httpResource can be used in the template to control which elements should be displayed.

rxResource

  • Source is defined as RxJS Observable.

  • Instead of a loader function, it accepts a stream function that accepts an RxJS Observable.

  • Same APIs as resource.

rxResource

export class UserProfile {
  // Relies on a service that exposes data through an RxJS Observable.
  private userData = inject(MyUserDataClient);
  protected userId = input<string>();
  private userResource = rxResource({
    params: () => ({ userId: this.userId() }),
    stream: ({params}) => this.userData.load(params.userId),
  });
}

Comparison

Feature resource httpResource() rxResource
Primary Use Case General async operations with Promises Simple HTTP GET requests Working with existing RxJS Observables
Data Source Any Promise-returning function Built-in HttpClient (GET only) RxJS Observable streams
Request Initiation Lazy (when accessed) Eager (immediate) Lazy (when accessed)
Best For Custom fetch logic, non-HTTP async operations Simple data fetching from REST APIs Integrating with existing RxJS services

RxJS Interop

@angular/core/rxjs-interop

APIs that help you integrate RxJS & Signals.

  • Create a signal from an RxJs Observable with toSignal.
     

  • Create an RxJS Observable from a signal with toObservable.
     

  • Create an output based on an RxJs Observable.
     

  • Creating an RxJS Observable from a component or directive output.
     

  • Unsubscribing with takeUntilDestroyed

toSignal

counterObservable = interval(1000);

// Get a Signal representing the counterObservable's value.
counter = toSignal(
           this.counterObservable,
           {initialValue: 0}
          );

toObservable

export class SearchResults {
  query: Signal<string> = inject(QueryService).query;
  query$ = toObservable(this.query);
  results$ = this.query$.pipe(
    switchMap(query => this.http.get('/search?q=' + query ))
  );
}
const obs$ = toObservable(mySignal);
obs$.subscribe(value => console.log(value));
mySignal.set(1);
mySignal.set(2);
mySignal.set(3);

outputFromObservable

@Directive({/*...*/})
class Draggable {
  pointerMoves$: Observable<PointerMovements> = listenToPointerMoves();
  
  // Whenever `pointerMoves$` emits, the `pointerMove` event fires.
  pointerMove = outputFromObservable(this.pointerMoves$);
}

outputToObservable

@Component(/*...*/)
class CustomSlider {
  valueChange = output<number>();
}
// Instance reference to CustomSlider.
const slider: CustomSlider = createSlider();
outputToObservable(slider.valueChange) // Observable<number>
  .pipe(...)
  .subscribe(...);

takeUntilDestroyed

export class UserProfile {
  private dispatcher = inject(NotificationDispatcher);
  private popup = inject(CustomPopupShower);
  constructor() {
    // This subscription the 'notifications' Observable is automatically
    // unsubscribed when the 'UserProfile' component is destroyed.
    const messages: Observable<string> = this.dispatcher.notifications;
    messages.pipe(takeUntilDestroyed()).subscribe(message => {
      this.popup.show(message);
    });
  }
}

Roadmap

Improving the Angular developer experience

Decision Tree

SoodAnkita
ankitasood.bsky.social
WebVibesOnly
GuacamoleAnkita

Techorama - Activating Angular's Reactive Core

By Ankita Sood

Techorama - Activating Angular's Reactive Core

  • 23