Adieu RxJS ! Vive les Signals ! Oh wait…

Anthony Pena
@_Anthony_Pena

Anthony Pena

Développeur Web Fullstack @ center

angulardevs.fr angulardevs.fr

@_Anthony_Pena

Avant les frameworks

@_Anthony_Pena

La Réactivité ?

@_Anthony_Pena

"A declarative programming model for updating based on changes to state."

-- Kristen / pzuraq

https://www.pzuraq.com/blog/what-is-reactivity

@_Anthony_Pena

Angular.JS et ses watchers

@_Anthony_Pena

Angular 2 et Zone.js

@_Anthony_Pena
@Component({
  template: `
    <p>{{ text }}</p>
  `,
})
export class PlaygroundComponent {
  text = "";

  ngOnInit() {
      setInterval(() => this.text += '!', 1_000)
  }
}
@_Anthony_Pena
@Component({
  template: `
    <input (change)="setText($event)"/>
    <p>{{ text }}</p>
  `,
})
export class PlaygroundComponent {
  text = "";

  setText(event: Event) {
    this.text = (event.target as HTMLInputElement).value;
  }
}
@_Anthony_Pena
@Component({
  template: `
    <input [(ngModel)]="text"/>
    <p>{{ text }}</p>
  `,
})
export class PlaygroundComponent {
  text = "";

  ngOnInit() {
      setInterval(() => this.text += '!', 1_000)
  }
}
@_Anthony_Pena
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <p>{{ text }}</p>
  `,
})
export class PlaygroundComponent {
  text = "waiting...";

  ngOnInit() {
      this.asyncHello().then(text => this.text = text);
  }

  private asyncHello(): Promise<string> {
    // ...
  }
}
@_Anthony_Pena
@Component({
  template: `
    <p>{{ text | async }}</p>
  `,
})
export class PlaygroundComponent {
  text = this.asyncHello();

  private asyncHello(): Promise<string> {
    // ...
  }
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  @Input() text: string;
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text  }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  @Input() text: string;
  @Output() textChange = new EventEmitter<string>();
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  @Input() text: string;
  @Output() textChange = new EventEmitter<string>();
  @ViewChild(Alert) alerts: Alert;
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  @Input() text: string;
  @Output() textChange = new EventEmitter<string>();
  @ViewChild(Alert) alerts: Alert;
  @ViewChildren(CustomCard) cards: QueryList<CustomCard>;
}
@_Anthony_Pena

C'est cool tout ça non ?

@_Anthony_Pena

Oui mais Zone.js

@_Anthony_Pena

Angular 17 et les Signals

@_Anthony_Pena
@Component({
  template: ` <p>{{ text() }}</p> `,
})
export class PlaygroundComponent {
  text = signal('');

  constructor() {
    setInterval(() => {
      this.text.set(this.text() + '!');
      // or
      // this.text.update((actual) => actual + '!')
    }, 1_000);
  }
}
@_Anthony_Pena
@Component({
  template: `
    <input (change)="setText($event)" />
    <p>{{ text() }}</p>
  `,
})
export class PlaygroundComponent {
  text = signal('');

  setText(event: Event) {
    this.text.set((event.target as HTMLInputElement).value);
  }
}
@_Anthony_Pena
@Component({
  template: `<p>{{ text() }}</p> `,
})
export class PlaygroundComponent {
  text = signal('waiting...');

  constructor() {
    this.asyncHello().then((text) => this.text.set(text));
  }

  private asyncHello(): Promise<string> {
    // ...
  }
}
@_Anthony_Pena
@Component({
  template: `
    <p>{{ text() }}</p>
    <p>{{ questionText() }}</p>
  `,
})
export class PlaygroundComponent {
  text = signal('');
  questionText = computed(() => this.text().replaceAll('!', '?'));

  constructor() {
    setInterval(() => {
      this.text.set(this.text() + '!');
    }, 1_000);
  }
}
@_Anthony_Pena
@Component({
  template: ` <p>{{ text() }}</p> `,
})
export class PlaygroundComponent {
  text = signal('');

  constructor() {
    setInterval(() => this.text.set(this.text() + '!'), 1_000);
    effect(() => {
      console.log('text =', this.text());
    });
  }
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  @Input() text: string;
  @Output() textChange = new EventEmitter<string>();
  @ViewChild(Alert) alerts: Alert;
  @ViewChildren(CustomCard) cards: QueryList<CustomCard>;
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text() }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  text = input('');
  @Output() textChange = new EventEmitter<string>();
  @ViewChild(Alert) alerts: Alert;
  @ViewChildren(CustomCard) cards: QueryList<CustomCard>;
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text() }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  text = input('');
  textChange = output<string>();
  @ViewChild(Alert) alerts: Alert;
  @ViewChildren(CustomCard) cards: QueryList<CustomCard>;
}
@_Anthony_Pena
@Component({
  template: `
    <app-alert>Alert</app-alert>
    <p>{{ text() }}</p>
    <app-card />
    <app-card />
  `,
})
export class PlaygroundComponent {
  text = input('');
  textChange = output<string>();
  alerts = viewChild(Alert);
  cards = viewChildren(CustomCard);
}
@_Anthony_Pena

Signals ❤️

@_Anthony_Pena

Bientôt un standard

https://github.com/tc39/proposal-signals

@_Anthony_Pena

Zone.js c'est fini ?

@_Anthony_Pena

Oups j'ai oublié RxJS dans tous ça 🙊 (non)

@_Anthony_Pena

Mais au fait... C'est quoi RxJS ?

@_Anthony_Pena
center

RxJS

@_Anthony_Pena
center

ReactiveX for JavaScript

@_Anthony_Pena
center
@_Anthony_Pena

Parlons Observable

@_Anthony_Pena
import { toObservable, toSignal } from '@angular/core/rxjs-interop';

@Component({})
export class PlaygroundComponent {
  text = signal('Hello');
  text$ = toObservable(this.text);
  textSignal = toSignal(this.text$);
}
@_Anthony_Pena
@_Anthony_Pena

Mais du coup les Signals...

@_Anthony_Pena

Quelques cas concrets

@_Anthony_Pena

HttpClient

@Component({
  template: ` <button (click)="clicked()">Get</button> `,
})
export class PlaygroundComponent {
  http = inject(HttpClient);

  clicked() {
    this.http.get('/axolotl').subscribe();
  }
}
@_Anthony_Pena

... et interceptor

export function authInterceptor(
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
  return next(
    req.clone({
      headers: req.headers.set('X-Quiz', 'Axolotl'),
    })
  );
}
@_Anthony_Pena

État interne des composants

@Component({
  template: ` <p>{{ text() }}</p> `,
})
export class PlaygroundComponent {
  text = signal('');
}
@_Anthony_Pena

Reactive Forms

@_Anthony_Pena

Services

@_Anthony_Pena

Global state management
(NgRx, NgXs)

@_Anthony_Pena

The NgXs way

@_Anthony_Pena

Classic way

@Component({ ... })
export class ZooComponent {
  animals$: Observable<string[]> = this.store.select(ZooState.getAnimals);

  constructor(private store: Store) {}
}
@_Anthony_Pena

Signal way

@Component({ ... })
export class ZooComponent {
  animals = select(ZooState.getAnimals);
}
@_Anthony_Pena

The NgRx way

@_Anthony_Pena
type BooksState = {
  books: Book[];
  isLoading: boolean;
  filter: { query: string; order: 'asc' | 'desc' };
};

const initialState: BooksState = {
  books: [],
  isLoading: false,
  filter: { query: '', order: 'asc' },
};

export const BooksStore = signalStore(
  withState(initialState),
  withComputed(/* ... */),
  withMethods((store, booksService = inject(BooksService)) => ({/* ... */}}))
);
@_Anthony_Pena
@Component({
  providers: [BooksStore],
})
export class BooksComponent {
  readonly store = inject(BooksStore);
}
@_Anthony_Pena

The NgRx way

export const BooksStore = signalStore(
  withMethods((store, booksService = inject(BooksService)) => ({
    loadByQuery: rxMethod<string>(
      pipe(
        debounceTime(300),
        distinctUntilChanged(),
        tap(() => patchState(store, { isLoading: true })),
        switchMap((query) => {
          /* ... */
        })
      )
    ),
  }))
);
@_Anthony_Pena

En résumé

@_Anthony_Pena

Les Signals c'est pour gérer les états et la réactivité dans les composants

@_Anthony_Pena

RxJS est là pour gérer tous vos flux de données

@_Anthony_Pena

https://x.com/BenLesh/status/1775207971410039230

@_Anthony_Pena

Anthony Pena

Développeur Web Fullstack @ center

@_Anthony_Pena_

@kuroidoruido

@penaanthony

https://k49.fr.nf

https://github.com/kuroidoruido/talks

angulardevs.fr angulardevs.fr

@_Anthony_Pena
black
@_Anthony_Pena

- Avant 2010 - grosso modo vanilla ou jQuery - on faisait tout à base de addEventListener - donc on ne se basait que sur les events standard JS - on écoutait des events mais pas plus que ça - on mixait le "comment mettre à jour" avec les données et la vue

- le fait de réagir ? - un peu trop basic comme définition

l'idée c'est de définir des données, les mapper et avoir un mécanisme qui réagit tout seul pour faire en sorte de synchroniser le tout

- le père de tous les frameworks modernes - fonctionnement à base de composant - two-ways data-binding - on map une variable de notre composant dans la vue - à chaque changement de la variable on change la vue - si la vue bouge, on change les variables en conséquence aussi - on part exemple un input dont la value était mappé à une variable, on avait un binding dans les deux sens - tout fonctionnait à base d'un système de watcher - ça marche tant que y'a pas grand chose à watch - c'est pas perf - plus y'a de watch plus c'est lourd et lent - on est limité au tick du watcher

- avant Angular 2 on cherche à faire mieux - fini le watch, on introduit Zone.js - à chaque fois qu'il se passe un truc, Zone prévient Angular qu'il doit lancer une détection de changement - là Angular fait le tour de tous les composants pour voir ce qui a pu changer (ce qu'on appelle la phase de "change detection") - déclenche une mise à jour des composants qui ont bougés

# CODE SLIDE : avec l'édition d'une variable

# CODE SLIDE : avec l'édition d'un champ de texte

# CODE SLIDE : avec du two-way data binding

# CODE SLIDE : avec la résolution d'une promesse en mappant le retour

# CODE SLIDE : avec la résolution d'une promesse en async pipe

- c'est simple à écrire (bien que parfois un peu verbeux) - c'est réactif - on y est habitué MAIS - quand même pas mal d'annotation, de type à mettre, et...

- Zone vient monkey patch pleins d'API standard pour introduire une mécanique de notification d'appel (pour les setTimeout, les setter, les Promises, etc.) - Zone ça fonctionne - c'est mieux que les watcher - mais on monkey patch beaucoup de choses - c'est lourd - y'a un système de contexte un peu bizarre - c'est un peu trop magique pour être facile à comprendre - Angular + Zone c'est forcément du component level pour la réactivité

- experimental à partir de la v16 - stable en v17 - nouvelle mécanique pour la réactivité - on ne se repose plus sur Zone pour la réactivité - on ne fait plus de réactivité à l'échelle du composant mais d'une variable - ce qu'on appelle la "fine grained reactivity" - le concept n'est pas nouveau, Solidjs l'a introduit en 2019

# CODE SLIDE : reprendre la demo edition de variable mais en Signal

# CODE SLIDE : reprendre la demo edition de champ de texte mais en Signal

# CODE SLIDE : reprendre la demo promesse mais en Signal

# CODE SLIDE : montrer computed

# CODE SLIDE : montrer effect

# CODE SLIDE : demo Signal input()

# CODE SLIDE : demo Signal output()

# CODE SLIDE : demo Signal viewchild() / viewChildren()

- moins d'anotation - moins de complexité dans les composants

- rien de compliqué - beaucoup plus léger - plus de magie sur la gestion de l'asynchrone

- en passe d'être standardisé dans ECMAScript (stage 1) - l'implémentation est simple - Annecdote : le polyfill actuel est basé sur l'implémentation des Signals d'Angular

- plus besoin de Zone - en tout cas bientôt - et on commence à pouvoir remplacer beaucoup de chose par des signals

- Pas besoin de RxJS pour les composants - RxJS n'est utilisé (en tout cas pas visible) dans la réactivité en Angular

- viens initialement de chez Netflix et plutôt côté backend

- avec les Signals on parlait de reactivité et d'état - avec RxJS on parle stream et event - le but de RxJS c'est de nous donner une API haut niveau pour gérer des données sur un flux, les combiner, transformer, filtrer, faire du rejeux, etc. - aucune notion d'UI - RxJS peut très bien s'utiliser côté backend ! - typiquement NestJS est en grande partie basé sur RxJS

- un Observable est stateless - un Observable est lazy - un Observable est purement fonctionnel et "immutable"

# CODE SLIDE : demo Signal + Observable toSignal() toObservable() - on a des ponts faciles au besoin entre RxJS et les Signals quand même

- API beaucoup plus simple mais aussi beaucoup plus limité - stateful donc attention à l'impact mémoire en fonction de ce qu'on fait - pas forcément simple de garder un arbre de dépendance entre signal simple

- Entièrement basé sur RxJS - Il faudrait casser l'interface du HttpClient pour changer ça - Il faudrait tout ré-écrire pour passer sur des Signals - Pas de raison de le faire - C'est déjà facile de passer d'un Observable à un Signals

- Et aussi les interceptors se basent sur le fait que c'est du RxJS - Donc HttpClient et interceptor => on reste sur RxJS

- Faire du full Signals pour être plus simple - Plus d'async, plus de fuite mémoire (ou presque) - ne plus utiliser RxJS à moins de ne pas avoir le choix (est-ce que ce n'est pas une erreur de design si on est obligé ?)

- basé sur RxJS - l'API expose des Observables - logique comme c'est des events - à priori ça ne changera pas

- Ça dépend mais je pencherais plutôt sur RxJS par défaut ou valeur simple - Si on sait que la valeur va beaucoup changer RxJS - Si la valeur bouge peu : valeur simple

- Basé sur RxJS - API pensé pour être simple avec RxJS - Exploite à fond les opérateurs RxJS - Optimisé pour RxJS

- Fournir des utilitaires qui permettent directement de faire le pont entre NgXs et nos composants sous forme de Signal

- Signal Store - Un store basé sur les Signals - Fourni des ponts pour utiliser du RxJS quand c'est plus pratique - encore en preview ! - créé par Brandon Roberts