Caso de estudio

Refactor del componente de configuración de cursos

Caso de estudio del refactor de grades-tab en el Sistema de Carga Horaria: de un componente monolítico basado en Context a una arquitectura escalable con Zustand y Repository Pattern.

Vander Luis Catti Idme ·
carga-horaria refactor zustand repository-pattern frontend

Caso de estudio del refactor arquitectónico del componente grades-tab —la configuración de cursos y asignaciones horarias de docentes— del Sistema de Cálculo de Carga Horaria: cómo pasó de un componente monolítico basado en Context a una arquitectura modular con Zustand y Repository Pattern.

Descarga: Artículo técnico (PDF) — versión 1.0, mayo 2026. Documento original por Vander Luis Catti Idme; esta página es la versión navegable equivalente.

Artículo relacionado: este caso de estudio cubre el frontend (componente de configuración de cursos). Para el backend (motor de cálculo de carga horaria en NestJS) ver Sistema de Cálculo de Carga Horaria.

El componente

El módulo grades-tab permite configurar los cursos de un colegio dentro de un período académico: qué asignaturas tiene cada curso, cuántas horas semanales, y qué docente está asignado a cada asignatura. Es una de las pantallas más usadas y de mayor complejidad de negocio del sistema.

Interfaz de configuración de cursos: lista de cursos a la izquierda y, a la derecha, las asignaturas del curso con sus horas y totales (datos de ejemplo)

Cada asignatura puede expandirse para ver y editar la asignación de docente (titular) y las horas correspondientes:

Vista expandida de asignaturas mostrando la asignación de un docente titular y las horas por asignatura (datos de ejemplo)

1. Introducción

A medida que una aplicación crece, decisiones técnicas que al inicio parecían simples comienzan a mostrar sus límites. Esto ocurre especialmente en módulos con alta complejidad de negocio, muchas interacciones de usuario y requerimientos en evolución constante.

Fue el caso de grades-tab. En su primera versión, el equipo optó por una implementación rápida basada en Context + useReducer, centralizando toda la lógica en un único hook. Funcionó bien en etapas iniciales, pero conforme el producto evolucionó aparecieron nuevos escenarios: filtros dinámicos, modales con estados independientes, sincronización de datos remotos, cálculos derivados, búsquedas reactivas, edición parcial de entidades, dependencias entre selects y múltiples flujos asincrónicos simultáneos.

Con el tiempo, el módulo se convirtió en un punto crítico de mantenimiento y escalabilidad. Este artículo describe el refactor hacia una arquitectura más modular y mantenible usando Zustand para el estado, Repository Pattern para el acceso a datos, separación explícita de responsabilidades y una reorganización estructural por dominio funcional.

2. Problemas de la implementación original

La arquitectura original centralizaba prácticamente toda la lógica en use-grades.ts, un archivo monolítico de más de 550 líneas. Ese hook era responsable de obtener datos desde Apollo, ejecutar mutations, manejar el estado de UI, procesar filtros, controlar modales, almacenar datos derivados, ejecutar lógica de negocio y coordinar actualizaciones entre componentes.

2.1 Alta cohesión accidental

Muchas responsabilidades distintas convivían en el mismo hook:

ResponsabilidadEjemplo
Estado de negociocursos, profesores, subjects
Estado visualmodales, loading, filtros
Sincronización remotaApollo queries/mutations
Datoscálculos y mapeos
Side effectsrefreshes y sincronización

Cualquier cambio pequeño podía impactar múltiples partes del sistema.

2.2 Complejidad creciente del reducer

La solución usaba useReducer con múltiples acciones centralizadas (SET_GRADES, OPEN_MODAL, UPDATE_FILTER, SET_LOADING…). Con el crecimiento del módulo aumentó la cantidad de actions, se incrementó el acoplamiento y el reducer empezó a comportarse como un “mini framework interno”. El problema de fondo no era useReducer en sí, sino que todo el dominio dependía de un único árbol de estado compartido.

2.3 Re-renders innecesarios

Al usar Context como mecanismo principal de distribución de estado, cualquier actualización disparaba renders amplios, incluso en componentes que no necesitaban esos cambios. Esto era especialmente problemático en las listas y en el cálculo de horas docentes: una asignación afecta a la asignatura, que a su vez afecta al curso.

2.4 Acoplamiento entre UI y capa de datos

Apollo Client estaba integrado directamente dentro del hook principal (useQuery, useMutation). Esto generaba dependencia fuerte del cliente GraphQL, dificultad para testear, poca reutilización y lógica de red mezclada con lógica de UI.

3. Objetivos del refactor

Antes de migrar se definieron objetivos arquitectónicos claros: separar responsabilidades, reducir acoplamiento, mejorar mantenibilidad, facilitar testing, minimizar renders innecesarios, permitir escalabilidad funcional y mejorar legibilidad.

4. Nueva arquitectura

La nueva implementación introduce una arquitectura basada en capas y responsabilidades explícitas.

Comparación de la arquitectura: antes un hook monolítico que mezclaba todo; después componentes que usan hooks ligeros, stores de Zustand y repositories sobre Apollo
AspectoOriginal (grades-tab)Refactor (grades-tab-refactor)
Manejo de estadoContext + useReducerZustand
OrganizaciónMonolíticaModular
Acceso a datosApollo directoRepository Pattern
ResponsabilidadesMezcladasSeparadas
RenderizadoGlobalSelectivo
EscalabilidadLimitadaAlta
TestingDifícilMás simple

4.1 Separación del estado con Zustand

Uno de los cambios más importantes fue reemplazar Context por múltiples stores independientes:

stores/
├── grade.store.ts
├── grade-ui.store.ts
├── grade-subject.store.ts
└── selectors-data.store.ts
Los cuatro stores de Zustand y su responsabilidad: grade (negocio de cursos), grade-subject (asignaturas), grade-ui (estado visual) y selectors-data (datos auxiliares)
  • grade.store.ts — estado de negocio de cursos: listado de grades, CRUD, sincronización de entidades, loading states específicos.
  • grade-subject.store.ts — estado de negocio de asignaturas: asignación de docentes, horas y cálculos derivados.
  • grade-ui.store.ts — estado puramente visual: búsqueda, modales, filtros, estados temporales de interacción. Evita contaminar la lógica de negocio con detalles de presentación.
  • selectors-data.store.ts — información auxiliar: people, subsidies, datos para selects. Separarlos reduce dependencias cruzadas y evita estados gigantes.

4.2 Beneficios obtenidos con Zustand

Renderizado selectivo. Gracias a los selectores y useShallow, los componentes solo reaccionan a cambios relevantes, reduciendo significativamente los renders innecesarios.

Comparación de renderizado: con Context un cambio de filtro re-renderiza todos los componentes; con Zustand y useShallow solo se re-renderiza el componente que depende de ese cambio

Menor boilerplate. Se eliminó gran parte del código de reducers, actions, dispatches y constantes de tipos. El estado se volvió más directo y expresivo.

4.3 Implementación del Repository Pattern

Otro cambio fundamental fue desacoplar el acceso a datos. En la solución original, Apollo estaba embebido en el hook, lo que generaba dependencias difíciles de mockear, lógica repetida y complejidad accidental. Se introdujo una capa de repositories:

repositories/
├── grade.repository.ts
├── grade-subject.repository.ts
└── selectors-data.repository.ts
Repository Pattern: antes los componentes y hooks usaban Apollo directamente; después consumen métodos del dominio expuestos por un repository que encapsula Apollo

Los componentes y hooks ya no conocen Apollo directamente; ahora solo consumen métodos del dominio (gradeRepository.updateGrade(data)). Esto permite reutilizar la lógica de acceso a datos desde hooks, servicios, background sync, testing, loaders o futuras migraciones.

Hooks más livianos. En la arquitectura original los hooks eran a la vez state manager, orchestrator, data layer, UI controller y business layer. En el refactor pasan a ser coordinadores ligeros: use-grades.ts (más de 550 líneas, “todo”) se transformó en use-grade.ts (~60 líneas, sincronización mínima).

4.4 Reorganización de componentes

La estructura original mezclaba parcialmente responsabilidades. La nueva sigue dominios funcionales claros:

grades-tab-refactor/
├── components/
│   ├── grade-details/
│   └── modals/
├── hooks/
├── repositories/
├── stores/
└── utils/

4.5 Mejoras técnicas introducidas

  • Debounce en búsquedasdebounce(search, 300) para evitar renders y consultas excesivas.
  • Selectores optimizados — uso de useShallow() para minimizar renders.

5. Resultados

  • Corrección de rendimiento. Mientras más se usaba la pestaña de grades, se creaban instancias de Context innecesarias, lo que producía un memory leak que obligaba a refrescar toda la página. El refactor lo eliminó.
  • Mejor mantenibilidad. Es más fácil localizar responsabilidades.
  • Menor acoplamiento. UI, estado y acceso a datos quedaron desacoplados.
  • Mejor experiencia de desarrollo. Navegación más simple, archivos pequeños, debugging más claro y testing más sencillo.
  • Mayor escalabilidad. Agregar nuevas funcionalidades requiere menos impacto transversal.

6. Conclusión

El refactor de grades-tab representa una evolución arquitectónica importante: de una solución monolítica hacia una arquitectura modular, desacoplada y preparada para crecer. Los cambios principales fueron la migración de Context a Zustand, la separación explícita del estado, la introducción del Repository Pattern, la reducción de hooks gigantes, la reorganización estructural y la optimización del renderizado.

Más allá de la tecnología utilizada, el principal aprendizaje fue entender que el problema no era únicamente el tamaño del componente, sino la mezcla de responsabilidades dentro del mismo flujo. Separar el dominio correctamente permitió transformar un módulo difícil de mantener en una arquitectura mucho más predecible, escalable y extensible.


Documento técnico original por Vander Luis Catti Idme — versión 1.0, mayo de 2026. Descargar PDF.