Rethinking Reactivity using Angular Signals 🚦

What is reactivity?

Change propagation through dependencies tracking

Custom implementation

FirstName LastName FullName
Adam Clarke Adam Clarke
Phil Smith Phil Smith
firstName = 'Adam';

lastName = 'Clarke';

fullName = `${this.firstName} ${this.lastName}`;
console.log(this.fullName); // Adam Clarke

updateFirstName() {
  this.firstName = 'Mr. Adam';
}

this.updateFirstName();
console.log(this.fullName); // Adam Clarke
get fullName() {
  return `${this.firstName} ${this.lastName}`;
}

pankajparkar

Angular Signal🚦

  • helps to update application state in optimised way
  • implementation is based on reactive principals
  • callable function 
const count = signal(1); // WritableSignal<number>
const age = signal<number>(18); // WritableSignal<number>
const user = signal<User>({
  id: 1,
  name: 'Pankaj'
}); // WritableSignal<User>

pankajparkar

How signal API looks like in Angular?

export type Signal<T> = {
  () => T;
  [SIGNAL]: unknown;
};

export interface WritableSignal<T> extends Signal<T> {

  set(value: T): void;

  update(updateFn: (value: T) => T): void;

  asReadonly(): Signal<T>;
}
const count = signal(1); // WritableSignal<number>

pankajparkar

Signal Primitives

✍️ writable

🔢 📐🔀 computed

💥 effect

pankajparkar

✍️ writable🚦

const count = signal(0); // WritableSignal<number>
const count = signal<number>(0);

// Signals are getter functions
// calling them reads their value.
console.log('The count is: ' + count());

// Set value directly
count.set(3);

// Check count value is 
console.log('The count is: ' + count()); // 3

// if want to update value based on last value
count.update(value => value + 1);

// Check count value again 
console.log('The count is: ' + count()); // 4

Reference: Stackblitz

pankajparkar

🔢 📐🔀  computed🚦

const firstName = signal('Pankaj');
const lastName = signal('Parkar');

const fullName = computed(
  () => `${firstName()} ${lastName()}`
);

console.log('Firstname is ', firstName()); // 'Pankaj Parkar'

function updateFirstName() {
  firstName.set('Pankaj 1');
}

updateFirstName();

console.log('FullName is ', fullName());  // 'Pankaj 1 Parkar'

pankajparkar

Reference: Stackblitz

💥 effect

class TestComponent {
  firstName = signal('Pankaj');
  lastName = signal('Parkar');

  constructor() {
    effect(() => `First name is ${this.firstName()}`);

    effect(() => `Last name is ${this.lastName()}`);
  }

  updateName() {
    this.firstName.set('Pankajjj');
    this.lastName.set('Parkarr');
  }
}

pankajparkar

Signal implementation

@Component({
  ...,
})
export class SignalComponent {
  firstName = signal('Pankaj');
  lastName = signal('Parkar');
  fullName = computed(() => 
    `${this.firstName()} ${this.lastName()}`
  );

  firstNameEffect = effect(() => 
    console.log(`First Name is ${this.firstName()}`)
  );

  updateFirstName() {
    this.firstName.set('Pankajj');
    this.lastName.set('Pankajj');
  }
}
<table>
  <tr>
    <th>First Name</th>
    <th>Last Name</th>
    <th>Full Name</th>
  </tr>
  <tr>
    <td>{{firstName()}}</td>
    <td>{{lastName()}}</td>
    <td>{{fullName()}}</td>
  </tr>
</table>

<button (click)="updateFirstName()">
  Update FirstName
</button>

Reference: Stackblitz

Reference: Stackblitz

pankajparkar

fName lName fullName Points
Adam Clarke Adam Clarke 1
Phil Smith Phil Smith 2
Total 3

Emp

Spreadsheet

1st row

Clarke

Adam

1

Phil

Smith

2

2nd row

Total: 3

Adam Clarke

Phil Smith

🧠 mental model

🔢 📐🔀 computed - derived value

💥 effect - react to signal changes

✍️ signal - define state / value

pankajparkar

RxJS interop

const obs$ = toObservable(mySignal);
obs$.subscribe(
  value => console.log(value)
);

mySignal.set(1);
mySignal.set(2);
mySignal.set(3);
@Component(...)
export class SearchResults {
  // query is signal stored in service
  query = inject(QueryService).query;
  query$ = toObservable(this.query);

  results$ = this.query$.pipe(
    switchMap(query => 
      this.http.get('/search?q=' + query )
    ),
  );
}
  • Convert signal to observable and vice versa
  • imported from @angular/core/rx-interop 

pankajparkar

RxJS interop

import { Component } from '@angular/core';
import { toSignal } from '@angular/core/rx-interop';
import { interval } from 'rxjs';

@Component({
  template: `{{ counter() }}`,
})
export class Ticker {
  counterObservable = interval(1000);
  
  // Get a `Signal` representing the `counterObservable`'s value.
  counter = toSignal(this.counterObservable, {
    initialValue: 0,
  });
  
  // another example
  formValues = toSignal(this.form.valueChanges);
}

toSignal

pankajparkar

future: Signal based components

  • input and output 
  • view queries viewChild, viewChildren, contentChild, contentChildren
  • only ngOnInit and ngOnChanges hooks will be relevant
  • `computed` and `effect can replace rest of hooks 
  • new hooks afterRender, afterRenderEffect, afterNextRender
  • zoneless angular

pankajparkar

future: Signal based components

@Component({
  signals: true,
  selector: 'user-profile',
  template: `
    <p>Name: {{ firstName() }} {{ lastName() }}</p>
    <p>Account suspended: {{ suspended() }}</p>
  `,
})
export class UserProfile {
  // Create an optional input without an initial value.
  firstName = input<string>(); // Signal<string|undefined>

  // Create an input with a default value
  lastName = input('Smith'); // Signal<string>

  // Create an input with options.
  suspended = input<boolean>(false, {
    alias: 'disabled',
  }); // Signal<boolean>
  
  updated = output<boolean>(); // EventEmitter<boolean>;
}

pankajparkar

Take aways 📌

  • Reactivity analogy
  • Mental model to use Signal
  • Don't replace complete RxJS with Signals
  • Use rx-interop to deal with observable with signals
  • Don't write signal from effect
  • Fine grain reactivity - due to local change detection

pankajparkar

Q & A

pankajparkar

pankajparkar

Rethinking reactivity using Angular Signals

By Pankaj Parkar

Rethinking reactivity using Angular Signals

Rethinking reactivity using Angular Signals

  • 104