• Google Developer Expert for Angular
  • Senior Angular Developer  @ ASI
  • Co-organizer of Angular Athens Meetup
  • Angular Content Creator

Fanis Prodromou

/prodromouf

https://blog.profanis.me

@prodromouf

Angular Signals:

A Look 👀

Under the Hood

& Beyond

- What and Why 

- Under the hood

- Signal APIs

- httpResource

- Q & A

Angular Signals is the new reactivity system that improves:

DevEx

UX

Performance

DevEx

Performance

UX

UX

DevEx

Performance

signal inputs

model input

signal queries

effect

afterRenderEffect

computed

linkedSignal

resource API

UX

DevEx

Performance

more fine grained CD

glitch free rendering

UX

DevEx

Performance

more fine grained CD

fewer CD cycles

Zoneless

- smart variables that notify anyone who's interested when their value changes.

What are Signals?

- Traditional change detection checks everything. Signals allow tracks specific values

- Traditional Change Detection can be slow on complex applications

Why Signals?

- zone.js is great but triggers the CD multiple times 

Zone.js

User Interaction

dom event (click)

zone.js

(Angular Zone)

Change Detection

UI Update

Angular checks the entire component tree when the micro-task queue is empty

Default + Observable

OnPush + Observable

OnPush + Signals

OnPush + Observable

OnPush + Signals

OnPush + Signals

Default + Observable

OnPush + Observable

OnPush + Observable

Dirty

Dirty

Dirty

The Solution

Let's Signal

Consumers

Producers

Observers

Subject

Consumers

Producers

Effect

Template

counter

Consumers

Producers

Effect

Template

counter

Consumers

Producers

counter = signal<number>(0);

Producer

counter = signal<number>(0);

Returns a WritableSignal

Producer

counter = signal<number>(0);

Define the type

Producer

counter = signal<number>(0);

Default value

Producer

counter = signal<number>(0);

Producer

<div>
  {{ counter() }}
</div>

Consumer

Template Context

Consumer

effect(() => {
  console.log(this.counter());
});

Context

Consumer

Consumer

const evenOrOdd = 
      computed(() => counter() % 2 === 0 ? 'even' : 'odd');

Consumer & Producer

Producer

Consumer

The Graph

const counter = signal(0);
const counter = signal(0);
<div> {{ counter() }} </div>
<div> {{ counter() }} </div>
<div> {{ counter() }} </div>
<div> {{ counter() }} </div>
const evenOrOdd = 
	computed(() => counter() % 2 === 0 ? 'even' : 'odd');
<div> {{ counter() }} </div>
const evenOrOdd = 
	computed(() => counter() % 2 === 0 ? 'even' : 'odd');
<div> {{ counter() }} </div>
const evenOrOdd = 
	computed(() => counter() % 2 === 0 ? 'even' : 'odd');

"Since Angular knows how the data flows, can have a more fine-grained change detection"

Signals

Signals

Traversal

Traversal

Traversal

Pull - Push

const isValid = signal(true);
const username = signal('profanis');

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');
effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

effect

username

Push (notification)

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

username

Push (notification)

effect

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

username

Pull (value)

effect

effect(() => {
  console.log(`${isValid()} - ${username()}`);
});

isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

username

Push (notification)

effect

Consumers

Producers

isValid

username

Push (notification)

effect(() => {
  console.log(`${isValid()} - ${username()}`);
});

isValid.set(false);
username.set('profanis2');

effect

Consumers

Producers

isValid

username

Pull (value)

Pull (value)

effect(() => {
  console.log(`${isValid()} - ${username()}`);
});

isValid.set(false);
username.set('profanis2');

effect

effect

isValid

username

Q: Can I have the same magic 🪄 in HTTP calls?

? One
? Two
? Three

? One

makeHttpCall() {
  this.isLoading = true;
  
  this.postsService.get(userId).pipe(
  	finalize(() =>  this.isLoading = false)
  )
}
  return (source: Observable<any>): Observable<any> => {
    return new Observable((observer) => {
      const subscription = source.subscribe({
        next: (value) => {
          // is loading
        },
        error: (error) => {
          // is not loading
        },
        complete: () => {
          // is not loading
        },
      });

      return () => {
        // clean up
      };
    });
  };
this.postsService.get().pipe(
  customRxJsOperator(loadingState)
)

// loadingState.isLoading

? Two

makeHttpCall() {
  this.isLoading = true;
  this.hasError = false;
  
  this.postsService.get(userId).pipe(
    catchError((error) => {
      this.hasError = true;
      return of(null);
    }),
    finalize(() => this.isLoading = false)
  )
}
  return (source: Observable<any>): Observable<any> => {
    return new Observable((observer) => {
      const subscription = source.subscribe({
        next: (value) => {
          // is loading
          // no error
        },
        error: (error) => {
          // is not loading
          // has error
        },
        complete: () => {
          // is not loading
          // no error
        },
      });

      return () => {
        // clean up
      };
    });
  };
this.postsService.get().pipe(
  customRxJsOperator(loadingState)
)

// loadingState.isLoading
// loadingState.hasError
this.anotherService.get().pipe(
    // forgot to use the custom operator
)

// loadingState.isLoading
// loadingState.hasError

? Three

signal<string>

signal<string>

signal<string>

cancel

cancel

How about effect?

time - 0

time - 1

request

request

100ms

500ms

time - 0

time - 1

request

request

100ms

500ms

time - 0

time - 1

request

request

100ms

500ms

time - 0

time - 1

request

request

100ms

500ms

Sync

Async

At some point we will have the data

At some point we will have the data

We will always have data

http

isLoading

error

data

http

isLoading()

error()

data()

httpResource

httpResource makes a reactive HTTP request and exposes the request status and response value

httpResource(
	?, 
	?
)
httpResource(
	string | object | function , 
	?
)
httpResource(
	string | object | function , 
	options
)
// String
httpResource(`https://api.com/${signalValue()}`)

dependency

// Object
httpResource(
	{
		url: `https://api.com/${signalValue()}`,
		method: 'GET',
		params: { type: `${queryParamSignalValue()}` }
	}
)

dependency

// Object
httpResource(
	{
		url: `https://api.com/${signalValue()}`,
		method: 'GET',
		params: { type: `${queryParamSignalValue()}` }
	}
)

dependency

// Object
httpResource(
	{
		url: `https://api.com/${signalValue()}`,
		method: 'GET',
		params: { type: `${queryParamSignalValue()}` }
	}
)

Http verb

// Function
httpResource(() => `https://api.com/${signalValue()}`)
// Function
httpResource(() => 
             signalValue() ? 
             `https://api.com/${signalValue()}` : 
             undefined
)
// String with Options
httpResource(`https://api.com/${signalValue()}`, {
	defaultValue: {},
	parse: (response) => zodSchema.parse(response),
})
// String
resource = httpResource(`https://api.com/${signalValue()}`)
@if (resource.isLoading()) {
    <div>Loading...</div>
}

@if (resource.error()) {
    <div>Oops...</div>
}

@if (resource.value()) {
    <div>{{ resource.value() }}</div>
}
// String
resource = httpResource(`https://api.com/${signalValue()}`)
derivedState = computed(
  () => resource.value().map(it => it.title)
)

? Loading
 

? Loading
? Error
 

? Loading
? Error
? Data

Q: How can I cancel the previous http calls?

A: The httpResource behaves similar to switchMap

Q: How can I debounce the calls?

A: We should debounce the value and not the calls

DevEx

Performance

UX

linkedSignal

listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = computed(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length,
});
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length, // 3
});
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3', 'item4']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length, // 4
});
countOfItems.set(0)
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);


// keeps the selected item
selectedItem = listOfItems[0];
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);



// an HTTP call is happening
http.get().subscribe(data => 
	this.listOfItems.set([...data])
)
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);





selectedItem = signal<Item | null>(null);
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);




selectedItem = linkedSignal({
  source: this.listOfItems,
  computation: (items, previous) =>
    items.find((item) => item.name === previous?.value.name),
});

DevEx

Thank you !!

/prodromouf

https://blog.profanis.me

@prodromouf

Code Shots With Profanis

searchInput = new FormControl('');

searchInputDebounced$ = this.searchInput.valueChanges.pipe(
  debounceTime(500)
);

searchInputDebounced = toSignal(this.searchInputDebounced$);

optionsResource = httpResource<RecipeResponse>(
  `https://api.com?q=${this.searchInputDebounced()}`
);

Signal APIs

signal

inputs

new output

model input

signal queries

signal

inputs

model input

signal queries

new output

model input

signal queries

new output

signal

inputs

model input

signal queries

new output

signal

inputs

model input

signal queries

new output

signal

inputs

signal

inputs

@Component({...})
export class MyComponent {
  @Input() isChecked = false;
}
@Component({...})
export class MyComponent {
  isChecked = input(false);
}

read-only signal

export interface UserModel {
  name: string;
  age: number;
  
  /*Social*/
  address: string;
  twitter: string;
  linkedin: string;
  github: string;
  instagram: string;
  facebook: string;
  website: string;
  email: string;
}
@Component({...})
export class MyComponent implements OnChanges {
  @Input({ required: true }) user!: UserModel;
  userSocials: string[] = [];

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.user) {
      const { name, age, ...userSocials } = this.user;
      this.userSocials = Object.values(userSocials);
    }
  }
}
@Component({...})
export class MyComponent {
  user = input.required<UserModel>();
  userSocials = computed(() => {
    const { name, age, ...userSocials } = this.user();
    return Object.values(userSocials);
  });
}

model input

@Component({...})
export class ChildComponent {
  @Input({ required: true }) name!: string;
  @Output() nameChange = new EventEmitter<string>();
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child 	[name]="username" 
				(nameChange)="changeHandler($event)" />
  `,
})
export class ParentComponent { 
   username = 'profanis';
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child 	[name]="username" 
				(nameChange)="changeHandler($event)" />
  `,
})
export class ParentComponent { 
   username = 'profanis';
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
  username = 'profanis';
}
@Component({...})
export class ChildComponent {
  // @Input({ required: true }) name!: string;
  // @Output() nameChange = new EventEmitter<string>();
  name = model<string>();
}
@Component({...})
export class ChildComponent {
  // @Input({ required: true }) name!: string;
  // @Output() nameChange = new EventEmitter<string>();
  name = model.required<string>();
}
@Component({...})
export class ChildComponent {
  name = model<string>(); // writable signal

  addTitle() {
    this.name.update((name) => `Mr. ${name}`);
  }
}
@Component({...})
export class ChildComponent {
  name = model<string>(); // writable signal

  titleExists = computed(() => this.name().startsWith('Mr.'));
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
  username = 'profanis';
}

new output

@Component({...})
export class ChildComponent {
  name = input.required<string>();
  @Output() nameChange = new EventEmitter<string>();
}
@Component({...})
export class ChildComponent {
  name = input.required<string>();
  nameChange = output<string>()
}
@Component({...})
export class ChildComponent {
  @Output() formIsValid = this.form.statusChanges.pipe(
     map((status) => status === 'VALID'),
  );
}
import { outputFromObservable } from '@angular/core/rxjs-interop';

@Component({...})
export class ChildComponent {
  formIsValid = outputFromObservable(
    this.form.statusChanges.pipe(
            map((status) => status === 'VALID'))
  );
}

Optional RxJS

import { outputToObservable } from '@angular/core/rxjs-interop';

@Component({...})
export class ChildComponent {
  name = input.required<string>();
  nameChange = output<string>()
  nameChange$ = outputToObservable(nameChange)
}

What 

& How

Consumers

Producers

Effect

Template

counter

Template

Effect

counter.set(1);

Produces new value

Consumers

Producers

Effect

Template

counter.set(     )

Template

Effect

1

Consumers

Producers

Effect

Template

counter.set(1)

Template

Effect

1

Consumers

Producers

Effect

Template

counter.set(1)

Template

Effect

1

Consumers

Producers

Effect

Template

Template

Effect

1

counter.set(1)

1

Consumers

Producers

Effect

Template

Template

Effect

1

counter.set(1)

1

Consumers

Producers

Effect

Template

Template

Effect

1

counter.set(1)

1

Consumers

Producers

Effect

Template

Template

Effect

counter.set(1)

Counter

Counter

Consumers

Producers

Effect

Template

Template

Effect

counter.set(1)

Counter

Counter

Edge

ref_con

ref_prod

Consumers

Producers

Effect

Template

Template

Effect

counter.set(1)

Counter

Counter

Edge

ref_con

ref_prod

Consumers

Producers

Effect

Template

Template

Effect

counter.set(1)

Counter

Counter

Edge

ref_con

ref_prod

Consumers

Producers

Effect

Template

Template

Effect

counter.set(1)

Counter

Counter

Edge

ref_con

ref_prod

Consumers

Producers

Effect

Template

Template

Effect

counter.set(1)

Counter

Counter

Edge

ref_con

ref_prod

Consumers

Producers

counter

const counter = signal(0);

Consumers

Producers

counter

const counter = signal(0);

effect(() => console.log(counter());

Effect

Consumers

Producers

counter

const counter = signal(0);

effect(() => console.log(counter());

Counter

Effect

Consumers

Producers

counter

const counter = signal(0);

effect(() => console.log(counter());

Counter

Effect

Effect

Consumers

Producers

counter.set(     )

const counter = signal(0);

effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

1

Consumers

Producers

counter

const counter = signal(0);

effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

1

Consumers

Producers

counter

const counter = signal(0);

effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

1

Consumers

Producers

counter

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

computed

Consumers

Producers

counter

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter.set(     )

1

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

1

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter());

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

1

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter() + ' is ' + evenOrOdd()));

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter() + ' is ' + evenOrOdd()));

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter() + ' is ' + evenOrOdd()));

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

computed

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter() + ' is ' + evenOrOdd()));

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

computed

Effect

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter() + ' is ' + evenOrOdd()));

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

computed

Effect

Consumers

Producers

const counter = signal(0);
const evenOrOdd = computed(() => counter() % 2 === 0 ? 'even' : 'odd');
effect(() => console.log(counter() + ' is ' + evenOrOdd()));

counter.set(1);

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

computed

Effect

Counter

Effect

Effect

Computed

Counter

Computed

counter

1

computed

Effect

console.log("1 is even")
console.log("1 is odd")
console.log("1 is even")
console.log("1 is odd")

Counter

Log

Log

Computed

Counter

Computed

counter

10

computed

Log

GLITCH