Overview
The Odontología Frontend uses a service-based state management approach, combining Angular’s traditional service singleton pattern with modern reactive primitives like Signals and RxJS Observables .
This application uses a pragmatic approach: services act as state containers, while signals and observables handle reactivity.
State Management Architecture
Three-Layer Approach
Services Singleton services store application state
Signals Reactive state within components
RxJS Asynchronous operations and event streams
Service-Based State
Injectable Services
All state management services use providedIn: 'root' for singleton behavior:
@ Injectable ({
providedIn: 'root'
})
export class PatientService {
// State is stored directly in the service
private patients : PatientData [] = [ ... ];
}
This pattern ensures:
Single source of truth
State persists across navigation
Shared state between components
No need for external state management libraries
Patient Service
The PatientService manages all patient-related state:
src/app/services/patient.service.ts
import { Injectable } from '@angular/core' ;
export interface PatientData {
id : number ;
nombre : string ;
edad : number ;
phone : string ;
email : string ;
address : string ;
medication_allergies : string ;
billing_data : string ;
health_status : string ;
family_history : string ;
ultimaVisita : string ;
proximaCita : string ;
estado : string ;
citas : Cita [];
tratamientos : Tratamiento [];
}
@ Injectable ({
providedIn: 'root'
})
export class PatientService {
private patients : PatientData [] = [ ... ];
getPatients () : PatientData [] {
return this . patients ;
}
getPatientById ( id : number ) : PatientData | undefined {
return this . patients . find ( p => p . id === id );
}
addPatient ( patient : any ) : void {
const newPatient : PatientData = {
... patient ,
id: this . patients . length + 1 ,
nombre: ` ${ patient . first_name } ${ patient . last_name } ` ,
ultimaVisita: '-' ,
proximaCita: '-' ,
estado: 'activo' ,
citas: [],
tratamientos: []
};
this . patients . push ( newPatient );
}
}
Key Patterns
While the service stores mutable state internally, components receive copies: getPatients (): PatientData [] {
return this . patients ; // Returns reference
}
// Better: Return a copy
getPatients (): PatientData [] {
return [ ... this . patients ];
}
The private patients array is not directly accessible, only through methods: private patients : PatientData [] = [];
// Controlled access
getPatients (): PatientData [] { /* ... */ }
addPatient ( patient : any ): void { /* ... */ }
Strong TypeScript interfaces define state shape: export interface PatientData {
id : number ;
nombre : string ;
// ... all required fields
}
Treatment Service
Manages dental treatment definitions and operations:
src/app/services/treatment.service.ts
export interface Tratamiento {
id : number ;
nombre : string ;
categoria : string ;
descripcion : string ;
duracion : number ;
precio : number ;
}
@ Injectable ({
providedIn: 'root'
})
export class TreatmentService {
private tratamientos : Tratamiento [] = [
{ id: 1 , nombre: 'Limpieza dental' , categoria: 'Preventiva' ,
descripcion: 'Limpieza profesional y eliminación de sarro.' ,
duracion: 45 , precio: 80 },
// ... more treatments
];
getTratamientos () : Tratamiento [] {
return this . tratamientos ;
}
addTratamiento ( tratamiento : Omit < Tratamiento , 'id' >) {
const id = this . tratamientos . length > 0
? Math . max ( ... this . tratamientos . map ( t => t . id )) + 1
: 1 ;
const newTratamiento = { ... tratamiento , id };
this . tratamientos . push ( newTratamiento );
return newTratamiento ;
}
updateTratamiento ( tratamiento : Tratamiento ) {
const index = this . tratamientos . findIndex ( t => t . id === tratamiento . id );
if ( index !== - 1 ) {
this . tratamientos [ index ] = tratamiento ;
}
}
deleteTratamiento ( id : number ) {
this . tratamientos = this . tratamientos . filter ( t => t . id !== id );
}
}
CRUD Operations Pattern
The service provides complete CRUD operations:
Create
Read
Update
Delete
addTratamiento ( tratamiento : Omit < Tratamiento , 'id' > ) {
const id = this . tratamientos . length > 0
? Math . max ( ... this . tratamientos . map ( t => t . id )) + 1
: 1 ;
const newTratamiento = { ... tratamiento , id };
this . tratamientos . push ( newTratamiento );
return newTratamiento ;
}
getTratamientos (): Tratamiento [] {
return this . tratamientos ;
}
updateTratamiento ( tratamiento : Tratamiento ) {
const index = this . tratamientos . findIndex ( t => t . id === tratamiento . id );
if ( index !== - 1 ) {
this . tratamientos [ index ] = tratamiento ;
}
}
deleteTratamiento ( id : number ) {
this . tratamientos = this . tratamientos . filter ( t => t . id !== id );
}
Appointment Service
Handles appointment scheduling and management:
src/app/services/appointment.service.ts
export interface AppointmentData {
id : number ;
fecha : string ;
hora : string ;
paciente : string ;
tratamiento : string ;
doctor : string ;
duracion : string ;
estado : 'confirmada' | 'pendiente' | 'completada' ;
asistido : 'sí' | 'no' | 'pendiente' ;
}
@ Injectable ({
providedIn: 'root'
})
export class AppointmentService {
private appointments : AppointmentData [] = [ ... ];
getAppointments () : AppointmentData [] {
return this . appointments ;
}
updateAppointmentStatus ( id : number , status : 'confirmada' | 'pendiente' | 'completada' ) : void {
const appointment = this . appointments . find ( a => a . id === id );
if ( appointment ) {
appointment . estado = status ;
}
}
updateAppointmentTime ( id : number , time : string ) : void {
const appointment = this . appointments . find ( a => a . id === id );
if ( appointment ) {
appointment . hora = time ;
}
}
addAppointment ( data : Omit < AppointmentData , 'id' | 'estado' | 'asistido' | 'duracion' >) : void {
const newId = this . appointments . length > 0
? Math . max ( ... this . appointments . map ( a => a . id )) + 1
: 1 ;
const newAppointment : AppointmentData = {
... data ,
id: newId ,
estado: 'pendiente' ,
asistido: 'pendiente' ,
duracion: '30 min'
};
this . appointments . push ( newAppointment );
}
}
Specialized Update Methods
The appointment service provides granular update methods:
updateAppointmentStatus ( id : number , status : 'confirmada' | 'pendiente' | 'completada' ): void
updateAppointmentTime ( id : number , time : string ): void
updateAppointmentAttendance ( id : number , asistido : 'sí' | 'no' | 'pendiente' ): void
This pattern:
Provides clear, focused APIs
Ensures type safety with union types
Makes state changes explicit and trackable
Component State with Signals
Components use Angular Signals for local, reactive state:
export class App {
private readonly router = inject ( Router );
// Signal for component state
protected readonly title = signal ( 'odontologia-frontend' );
// Signal derived from Observable
private readonly currentUrl = toSignal (
this . router . events . pipe (
filter ( event => event instanceof NavigationEnd ),
map ( event => ( event as NavigationEnd ). urlAfterRedirects )
),
{ initialValue: this . router . url }
);
// Computed signal
protected showMenu = computed (() => {
const url = this . currentUrl ();
return url ? ! url . includes ( '/login' ) : true ;
});
}
Signal Patterns
Simple reactive state: protected readonly title = signal ( 'odontologia-frontend' );
// Update the signal
this . title . set ( 'New Title' );
// Read the signal value
const currentTitle = this . title ();
Derived state that automatically updates: protected showMenu = computed (() => {
const url = this . currentUrl ();
return url ? ! url . includes ( '/login' ) : true ;
});
// In template
@ if ( showMenu ()) {
< app - menu > </ app - menu >
}
Convert RxJS observables to signals: private readonly currentUrl = toSignal (
this . router . events . pipe (
filter ( event => event instanceof NavigationEnd ),
map ( event => ( event as NavigationEnd ). urlAfterRedirects )
),
{ initialValue: this . router . url }
);
RxJS for Asynchronous State
While not extensively used in the current codebase, RxJS is leveraged for event streams:
private readonly currentUrl = toSignal (
this . router . events . pipe (
filter ( event => event instanceof NavigationEnd ),
map ( event => ( event as NavigationEnd ). urlAfterRedirects )
),
{ initialValue: this . router . url }
);
When to Use RxJS
HTTP Requests Use observables for API calls and data fetching
Event Streams Handle complex event sequences and transformations
Async Operations Manage timers, intervals, and delayed actions
Multi-source Data Combine multiple data sources with operators
State Flow Patterns
Component → Service → Component
Typical data flow:
// Component retrieves state
export class Patient implements OnInit {
patients : PatientData [] = [];
constructor ( private patientService : PatientService ) { }
ngOnInit () : void {
// Fetch state from service
this . patients = this . patientService . getPatients ();
}
}
// Component modifies state
export class NewPatient {
constructor (
private patientService : PatientService ,
private router : Router
) {}
onSubmit ( formData : any ) {
// Update state through service
this . patientService . addPatient ( formData );
// Navigate to updated view
this . router . navigate ([ '/patient' ]);
}
}
Component-Local Computed State
Components can derive local state from service data:
src/app/patient/patient.ts
export class Patient implements OnInit {
searchText : string = '' ;
patients : PatientData [] = [];
get filteredPatients () {
if ( ! this . searchText ) {
return this . patients ;
}
const search = this . searchText . toLowerCase ();
return this . patients . filter ( p =>
( p . nombre ?. toLowerCase (). includes ( search ) || false ) ||
( p . email ?. toLowerCase (). includes ( search ) || false ) ||
( p . phone ?. includes ( search ) || false )
);
}
}
State Persistence
Currently, state is stored in-memory and resets on page refresh. For persistence, consider:
LocalStorage Pattern
@ Injectable ({
providedIn: 'root'
})
export class PatientService {
private readonly STORAGE_KEY = 'patients' ;
private patients : PatientData [] = [];
constructor () {
this . loadFromStorage ();
}
private loadFromStorage () : void {
const stored = localStorage . getItem ( this . STORAGE_KEY );
if ( stored ) {
this . patients = JSON . parse ( stored );
}
}
private saveToStorage () : void {
localStorage . setItem ( this . STORAGE_KEY , JSON . stringify ( this . patients ));
}
addPatient ( patient : any ) : void {
// ... add logic
this . saveToStorage ();
}
}
Best Practices
Services for Shared State Use services for state shared across multiple components
Signals for Component State Use signals for local, reactive component state
Immutable Updates Avoid direct mutation, create new objects/arrays
Strong Types Define interfaces for all state shapes
Single Responsibility Each service manages one domain of state
Encapsulation Keep state private, expose through methods
State Management Comparison
Pattern Use Case Example Service State Shared application state Patient list, treatments Signals Component-local reactive state UI toggles, derived values RxJS Observables Asynchronous operations HTTP requests, router events Component Properties Simple, non-reactive state Form values, temporary flags Getters Computed from component state Filtered lists, formatted data
Extending State Management
For more complex applications, consider:
Observable Services
@ Injectable ({
providedIn: 'root'
})
export class PatientService {
private patientsSubject = new BehaviorSubject < PatientData []>([]);
public patients$ = this . patientsSubject . asObservable ();
addPatient ( patient : any ) : void {
const current = this . patientsSubject . value ;
this . patientsSubject . next ([ ... current , patient ]);
}
}
// Component usage
export class Patient {
patients$ = this . patientService . patients$ ;
}
Signal-Based Services
@ Injectable ({
providedIn: 'root'
})
export class PatientService {
private patientsSignal = signal < PatientData []>([]);
public patients = this . patientsSignal . asReadonly ();
addPatient ( patient : any ) : void {
this . patientsSignal . update ( patients => [ ... patients , patient ]);
}
}
Next Steps
Architecture Learn about the application’s overall architecture
Routing Explore routing and navigation patterns