Filtros reactivos con observer pattern en JavaScript

Clase 9 de 24Curso de Manipulación Avanzada de Datos con JavaScript

Resumen

Mejora el rendimiento y la claridad de un habit tracker con filtros reactivos, un estado de filtros desacoplado y el observer pattern. Aquí verás cómo inyectar dependencias en tu habit service, suscribirte a cambios con subscribe y activar un engine multicriterio que filtra por frecuencia, status, racha mínima y texto con include. Además, se muestra el error que impedía la reactividad y cómo se resolvió con un simple notify.

¿Cómo se desacopla habit service con filtered state?

El primer paso es que el servicio reciba dos dependencias: repository y filtered state. Así se mantiene desacoplada la lógica de negocio del estado de filtros. Se introducen métodos como list filtered habits, un privado apply filters y un get state filters para exponer el estado cuando se necesite.

  • Inyección de dependencias para mantener bajo acoplamiento.
  • list filtered habits aplica filtros y retorna la lista correcta.
  • apply filters centraliza el filtrado y permite un engine multicriterio.
  • get state filters devuelve el estado actual de filtros.

¿Qué métodos nuevos organizan la lógica de filtros?

Se añade un método público para listar con filtros y uno privado para aplicarlos. Inicialmente, apply filters puede devolver los hábitos sin cambios mientras se construye el engine.

// Esqueleto orientativo class HabitService { constructor(repository, filteredState) { this.repository = repository; this.filteredState = filteredState; } listFilteredHabits() { // 1) obtener hábitos actuales del repository. // 2) leer filtros desde filteredState. // 3) aplicar filtros y retornar. return this.applyFilters(/*habits*/, /*filters*/); } // privado: motor de filtrado (temporalmente sin lógica) applyFilters(habits, filters) { return habits; // luego: lógica multicriterio. } getStateFilters() { return this.filteredState.getState(); } }

¿Cómo se usa el formulario de filtros sin filtrar ahí?

El formulario solo hace set del estado en filtered state. No filtra. La reacción ocurre después, gracias a la suscripción. Para ajustar la UI, se pidió al asistente (Cursor) modificar el form según el estado real de filtros: frequency, status, racha mínima y campo de texto, en lugar de un ejemplo de fecha.

  • Formulario limpio: solo set de filtros.
  • Adaptación de campos a frequency, status, racha y texto.
  • Petición con contexto de App.js para cambios precisos.

¿Cómo se implementa el observer pattern con subscribe y notify?

La clave está en el observer pattern: tras cada cambio en el estado de filtros, se notifica a los suscriptores y se renderiza la lista. Inicialmente no reaccionaba porque faltaba el notify dentro de set filters y reset. Al agregarlo, todo fue reactivo.

  • Suscriptores múltiples que reaccionan al mismo estado.
  • notify tras cada cambio o reset.
  • Subscribe dispara el render de hábitos.
// Esqueleto del estado observable class FilteredState { constructor() { this.subscribers = []; this.state = { frequency: 'all', status: 'all', minStreak: 0, text: '' }; } subscribe(fn) { this.subscribers.push(fn); } notify() { this.subscribers.forEach(fn => fn(this.state)); } setFilters(values) { this.state = { ...this.state, ...values }; this.notify(); // faltaba esta línea. } reset() { this.state = { frequency: 'all', status: 'all', minStreak: 0, text: '' }; this.notify(); } getState() { return this.state; } } // En la capa de vista filteredState.subscribe((current) => { renderHabits(); // reacción automática al cambio de filtros. });

¿Dónde falló la reactividad y cómo se corrigió?

El subscribe no se ejecutaba. La causa: no se llamaba a notify tras set filters. Al añadir notify en cada actualización y en reset, la UI volvió a renderizar hábitos de forma inmediata.

¿Cómo opera el motor multicriterio y el render de hábitos?

El engine de apply filters descarta por frecuencia, status, racha mínima y búsqueda por texto usando include. Si no hay filtros activos, se retorna toda la lista. Luego, render habits consume la lista ya filtrada.

  • Frecuencia: compara daily, weekly u otros valores.
  • Status: incluye activos o completados según se pida.
  • Racha mínima: descarta si la racha es menor.
  • Texto: filtra por coincidencia con include en nombre o descripción.
// Ejemplo orientativo de un motor multicriterio function applyFilters(habits, f) { if (!habits) return []; return habits .filter(h => f.frequency && f.frequency !== 'all' ? h.frequency === f.frequency : true) .filter(h => f.status && f.status !== 'all' ? h.status === f.status : true) .filter(h => f.minStreak ? h.streak >= f.minStreak : true) .filter(h => f.text ? (h.name || '').toLowerCase().includes(f.text.toLowerCase()) : true); } function renderHabits() { const filtered = habitService.listFilteredHabits(); // pintar la lista usando filtered. }

¿Cómo se probó el filtrado en la UI?

Se crearon dos hábitos: uno diario y otro semanal. Al activar el filtro por daily, solo apareció el hábito diario. Al filtrar por texto, coincidieron los que contenían la cadena buscada; con una cadena inexistente, no se mostró ninguno. El patrón demostró ser limpio y escalable.

  • Prueba por frecuencia: diario y semanal.
  • Prueba por texto con include.
  • Prueba sin coincidencias: lista vacía intencional.

¿Te gustaría que se añada persistencia con localStorage y ver el flujo completo guardando y restaurando filtros y hábitos? Comparte en comentarios qué campos te interesa conservar y cómo quisieras visualizar los resultados.