
Semana 5: Inmersión Profunda en una Gran Refactorización
Esta semana, decidí aparcar el desarrollo de nuevas funcionalidades para centrarme en un aspecto crucial y a menudo olvidado del desarrollo de software: la refactorización. A medida que el proyecto crecía, empecé a notar varios “code smells” (malos olores en el código) que hacían que la base del código fuera más difícil de mantener y escalar. Era hora de atacar de frente esta deuda técnica.
🚨 Identificando los Bad Smells
El primer paso fue analizar con lupa el estado actual de la aplicación e identificar las principales áreas problemáticas. Unos cuantos problemas cantaban a la legua:
1. El Componente Monolítico
El componente ProjectDetail.tsx
se había convertido en un monstruo. Estaba manejando el estado de las tareas, ideas, bocetos y posts del blog, lo que llevaba a más de 20 imports y cientos de líneas de código. Un componente con tantas responsabilidades es una clara “red flag” y era, sin duda, la parte peor diseñada de la aplicación.
// RED FLAG: Un componente con demasiados imports está haciendo demasiado.
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
// ... y más de 15 imports adicionales
2. Prop Drilling Excesivo
Para gestionar el estado, estaba pasando props a través de múltiples capas de componentes. Por ejemplo, una función onDeleteTask
podía viajar desde App.tsx
-> ProjectDetail.tsx
-> TasksSection.tsx
-> TaskList.tsx
-> TaskItem.tsx
. La mayoría de estos componentes intermedios ni siquiera usaban la función; solo la pasaban. Esto hace que los componentes estén fuertemente acoplados, sean difíciles de reutilizar y una pesadilla para depurar.
3. Dolores de Cabeza con el Estado y Desincronización
Más allá del prop drilling, encontré casos específicos donde la gestión del estado era frágil. Por ejemplo, en la funcionalidad del lienzo de dibujo, la lógica de deshacer/rehacer
dependía de una variable de estado externa, lo que podía provocar errores de desincronización.
// ANTES: Propenso a problemas de desincronización de estado
const saveToHistory = useCallback(() => {
// ...
setCanvasHistory(prev => {
const newHistory = prev.slice(0, historyIndex + 1); // Depende del `historyIndex` externo
newHistory.push(imageData);
return newHistory;
});
setHistoryIndex(prev => prev + 1);
}, [historyIndex]); // Dependencia de un estado externo
// DESPUÉS: Actualización de estado robusta usando la forma funcional
const saveToHistory = useCallback(() => {
// ...
setCanvasHistory(prev => {
const currentIndex = prev.length - 1;
const newHistory = prev.slice(0, currentIndex + 1);
newHistory.push(imageData);
return newHistory;
});
// Ya no hay dependencias externas para la lógica principal
}, []);
Este pequeño cambio es un ejemplo perfecto del principio: “Si estás actualizando un estado basándote en su valor anterior, usa siempre la forma funcional para evitar race conditions y dependencias fantasma.”
4. Servicios Repartidos y Problemas de Integridad de Datos
Los servicios para el almacenamiento local, las notificaciones (Swal
) y la manipulación de fechas estaban dispersos. Esta descentralización significaba que los componentes tenían dependencias directas de librerías específicas. Peor aún, provocaba problemas reales de integridad de datos. Por ejemplo, como las ideas se guardaban en una clave de localStorage diferente a la de los proyectos, al borrar un proyecto sus ideas asociadas quedaban huérfanas. Este es un síntoma clásico de una capa de datos desconectada.
🏗️ La Estrategia de Refactorización
Con los problemas identificados, tracé un plan multifase para mejorar sistemáticamente la arquitectura.
Fase 1: Domando el Monolito con Context
El problema más urgente era ProjectDetail
. La solución fue introducir la API de Context de React. Creé un ProjectProvider
que encapsula todo el estado y la lógica relacionados con un único proyecto. Esto aplicó el Principio de Inversión de Dependencias: los componentes de alto nivel ya no dependen de los detalles de implementación de los de bajo nivel. En su lugar, ambos dependen de una abstracción (el contexto).
Este enfoque tiene dos enormes ventajas:
- Elimina el Prop Drilling: Cualquier componente hijo dentro del proveedor ahora puede acceder al estado y a las acciones compartidas directamente usando el hook
useContext
. - Desacopla Componentes:
ProjectDetail
se convirtió en un simple componente “orquestador”, responsable únicamente de organizar a sus hijos dentro del proveedor.
Fase 2: Centralizando la Lógica con Hooks, Servicios y “Barrels”
Para combatir la lógica dispersa, tomé varias acciones clave:
- Creé un hook
useSwal
y un servicio: Estuve debatiendo si usar un patróncontext + hook
oservicio + hook
. Como la necesidad principal era mostrar mensajes sin que los componentes necesitaran saber sobre una cola de notificaciones, el patrón más simple deservicio + hook
era perfecto. Abstrae toda la lógica de notificaciones, así que si decido cambiar la librería más adelante, solo necesito actualizar el servicio. - Creé una clase
DateUtils
: Toda la lógica de formato y manipulación de fechas está ahora en un solo lugar, garantizando la coherencia. - Implementé Barrel Exports: Para limpiar los bloques masivos de imports, usé archivos
index.ts
para crear “barrels”. Esto consolida las exportaciones de una carpeta de funcionalidades, permitiéndome convertir muchas líneas de import en una sola línea limpia.
¿Qué Sigue en la Hoja de Ruta de Refactorización?
El trabajo está lejos de terminar, pero se ha sentado una base sólida. Aquí están los siguientes pasos:
- Hook Genérico
useForm
: Crear un hook personalizado para manejar el estado, la validación y la lógica de envío de todos los formularios de la aplicación. Esto reducirá drásticamente el código repetitivo. - Servicio de Repositorio Unificado: Consolidar todos los servicios de manejo de datos (
LocalStorageService
,SketchStorageService
, etc.) en una única claseRepository
. Esto proporcionará una API unificada para todas las operaciones de datos y, crucialmente, resolverá los problemas de integridad de datos como las ideas huérfanas. - Optimización del Rendimiento: Usar estratégicamente
useMemo
,useCallback
yReact.memo
para evitar re-renders innecesarios en componentes críticos, especialmente en vistas con muchos datos. - Librería de Componentes de UI: Construir un conjunto de componentes de UI base y reutilizables (
Button
,Input
,Modal
) para asegurar una apariencia consistente y acelerar el desarrollo futuro.
Este proceso de refactorización ha sido una tarea enorme, pero las mejoras en la calidad del código, la mantenibilidad y la experiencia de desarrollo ya están dando sus frutos. Es un recordatorio de que construir un gran software no consiste solo en añadir funcionalidades, sino también en cuidar los cimientos sobre los que se construye.