Skip to main content

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:
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;
}

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:
src/app/app.ts
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

PatternUse CaseExample
Service StateShared application statePatient list, treatments
SignalsComponent-local reactive stateUI toggles, derived values
RxJS ObservablesAsynchronous operationsHTTP requests, router events
Component PropertiesSimple, non-reactive stateForm values, temporary flags
GettersComputed from component stateFiltered 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

Build docs developers (and LLMs) love