Signals in un'app vera: il modulo di autenticazione di ApexFlow
Quando ho iniziato la tesi su ApexFlow, costruirla su Angular 19 e i Signals sembrava un azzardo. I Signals erano appena diventati stabili, quasi tutti i tutorial usavano ancora BehaviorSubject per tutto e io ci stavo per costruire sopra una dashboard CRM. Ripensandoci, è la scelta di cui sono più contento.
L'esempio più chiaro è il modulo di autenticazione: lo stato da cui dipende tutto il resto dell'app.
Il problema: uno stato da tenere sincronizzato
Un servizio di autenticazione risponde di continuo a poche domande. C'è qualcuno loggato? Chi? È un amministratore? Se lo chiedono l'header, la sidebar, le guard delle rotte e metà delle pagine.
La risposta classica è un BehaviorSubject per l'utente, un observable per ogni domanda derivata e un subscribe() in ogni componente che ne ha bisogno, più il takeUntil o la pipe async che fa pulizia. Funziona, ma lo stato finisce sparso in tanti flussi che speri restino sincronizzati, e ogni nuovo consumatore è un'altra sottoscrizione da gestire bene.
Due signal e una domanda derivata
In ApexFlow il servizio tiene due signal, letti una volta dallo storage all'avvio dell'app:
@Injectable({ providedIn: 'root' })
export class AuthService {
isLoggedIn = signal<boolean>(this.hasToken());
currentUser = signal<User | null>(this.getUserFromStorage());
isAdmin(): boolean {
return this.currentUser()?.role === UserRole.Admin;
}
}Tutto il resto è una lettura. Un template scrive auth.currentUser()?.name e Angular sa che quella vista dipende dall'utente. Una guard legge il valore in modo sincrono, senza flussi da aspettare né niente da cui disiscriversi:
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() ? true : router.createUrlTree(['/login']);
};Login e logout sono due chiamate a set(). Ogni vista che legge l'utente si aggiorna da sola, e non bisogna avvisare nessuno.
Cosa è sparito
- Le sottoscrizioni manuali. Nessun componente chiama
subscribe()per seguire lo stato di autenticazione, quindi non c'è niente da chiudere inngOnDestroye niente resta in memoria quando si lascia una pagina. - I bug di ordine. Un signal ha sempre un valore. Una guard non gira mai prima della prima emissione, perché una prima emissione non c'è.
- Le ipotesi nel debugger. Lo stato è un valore: metti un breakpoint ovunque e lo leggi, invece di aggiungere un
tapper vedere cosa è passato.
Dove resta RxJS
In ApexFlow i Signals hanno dato a RxJS un compito più preciso. Il tempo resta a RxJS: la chiamata HTTP del login è un Observable, e lo sono le ricerche con debounce e le richieste che devono annullare quella prima. I signal tengono ciò che è vero adesso; gli observable descrivono ciò che succede nel tempo. I due si incontrano con toSignal e toObservable, che ho usato per le tabelle lato server (il prossimo articolo).
Lo rifarei?
Sì, e dal primo commit. Se oggi inizi un progetto Angular, metti lo stato nei signal e tieni RxJS per gli eventi e il lavoro asincrono per cui è nato. Il codice si legge come una descrizione dello stato, non come una serie di istruzioni per tenerlo coerente.