Inicio / Artículos

Deuda frontend como costo de cambios: diagnosticando una base de código React en crecimiento

Aprenda a identificar las decisiones acumulativas que hacen que las aplicaciones grandes de React, Redux, TypeScript y Next.js sean costosas de modificar, y cómo reducir gradualmente ese costo.

Editor: REACTAPP.TOP18 min de lectura4156 palabras

Fuentes: Frontend Technical Debt: The Problems Nobody Notices Until the React App Becomes Huge

Las aplicaciones grandes en React casi nunca se vuelven difíciles de mantener debido a un único commit catastrófico. Se deterioran a través de una larga serie de decisiones pequeñas y, en apariencia, razonables: un useEffect adicional, una parte del estado de Redux que no necesitaba ser global, un componente “temporal” de 500 líneas, un widget compartido que sigue recibiendo más propiedades, un archivo utils que alberga todo aquello que no tiene un lugar evidente, un tipo de API pegado en tres módulos, una convención de estilo a la que nadie se atreve a modificar. Ninguna de estas decisiones parece incorrecta en el momento en que se toma. Los problemas comienzan cuando cientos de ellas se acumulan y empiezan a interactuar entre sí.

Esa acumulación es lo que entendemos por deuda técnica en el frontend, y la idea central de esta guía es sencilla: los mayores problemas del frontend rara vez son causados por un único fragmento de código defectuoso, sino por decisiones menores que se van acumulando. A continuación encontrará las formas más comunes en que esta deuda se manifiesta en bases de código de React, Redux, TypeScript y Next.js, por qué cada una se vuelve problemática, y una forma práctica de reducirla sin detener el trabajo del producto para realizar una reescritura.

Cómo se desvía un repositorio ordenado

Un nuevo proyecto suele comenzar con una estructura que cualquiera puede entender a simple vista:

src/
  components/
  pages/
  hooks/
  services/
  store/

Un desarrollador que nunca ha visto el repositorio puede adivinar dónde debería ir un nuevo hook o servicio. Luego, el producto crece, el equipo se expande y la lista de funcionalidades también aumenta. Unos años después, el mismo árbol estructural suele verse así:

src/
  components/
    Button/
    Modal/
    Table/
    UserTable/
    UserTableNew/
    CommonTable/
    GenericTable/
    SharedTable/
    ...
  hooks/
    useUser.ts
    useUsers.ts
    useUserData.ts
    useFetchUser.ts
    useUserDetails.ts
    ...
  utils/
    helpers.ts
    common.ts
    misc.ts
    formatter.ts
    newHelpers.ts
    oldHelpers.ts
  store/
    user/
    users/
    userData/
    userProfile/
    ...

Observe el patrón: varias tablas con funciones superpuestas, cinco mecanismos que obtienen “un usuario” de maneras ligeramente diferentes, archivos auxiliares diferenciados únicamente por las palabras “nuevo” y “viejo”, y cuatro carpetas de almacenamiento para lo que en realidad es un único dominio. Nadie diseñó esto; simplemente creció, pull request tras pull request, y esa es precisamente la razón por la que este tipo de deuda es peligroso: nunca hay un momento claro en el que sea evidente que necesite corrección.

El atajo que se vuelve permanente sin que nadie se dé cuenta

La fuente más común de deuda es la limpieza diferida. Una función debe lanzarse hoy mismo; su implementación adecuada tomaría dos días, mientras que la rápida solo requeriría tres horas. Elegir la versión de tres horas no implica necesariamente una mala práctica técnica; a veces es precisamente la decisión empresarial correcta.

El riesgo surge cuando el atajo sobrevive a su razón de existir. El código temporal tiende mucho a convertirse en infraestructura de producción, y las decisiones que lo crean suelen parecer inofensivas en el momento:

"Simplemente pongámoslo en el componente existente." ">Vamos a refactorizar esto después del lanzamiento." ">Por ahora, guardémoslo en Redux." ">Podemos generalizarlo más tarde." ">Nada más necesitará esto."

Medio año después, el contexto ya no existe. Nadie recuerda por qué se escribió el código de esa manera; la persona que lo escribió puede haberse trasladado a otro equipo, el requisito al que servía ha cambiado, y ahora tres otras funcionalidades dependen de su comportamiento. Deshacerse de un atajo que tomó tres horas puede llevar tres semanas.

Cuando un componente controla toda una funcionalidad

El “componente voluminoso” es uno de los síntomas más reconocibles. Tiende a comenzar con una lista razonablemente ordenada de tareas esbozadas dentro de una sola función:

function Checkout() {
  // fetch products
  // validate coupon
  // calculate totals
  // manage address
  // handle payment
  // track analytics
  // show modals
  // handle errors
  // render UI
}

Cada nueva exigencia acaba en el mismo lugar porque allí ya se encuentra el código relacionado. Con el tiempo, el componente pasa a encargarse de:

  • comunicación con la API
  • reglas de negocio y validación
  • gestión del estado
  • análisis y verificación de permisos
  • navegación y manejo de errores
  • estilización y renderizado

React en realidad no es el culpable aquí. El problema real es que demasiadas responsabilidades se han extendido más allá de los límites del componente. Un componente es un lugar adecuado para el renderizado y para el estado de la interfaz que pertenece a esa parte de la UI; no debería convertirse en el contenedor para todas las cuestiones relacionadas con una función específica. Las reglas de precios, la validación de cupones y los análisis pueden residir en módulos simples o hooks personalizados que se puedan probar sin necesidad de renderizar nada.

Componentes reutilizables que cuestan más de lo que ahorran

Al principio de una carrera, la reutilización parece ser sin duda algo positivo. Con la experiencia se hace evidente que toda abstracción tiene un costo. Considere una tabla que ha sido hecha “configurable”:

<DataTable
  data={data}
  columns={columns}
  loading={loading}
  selectable
  searchable
  sortable
  pagination
  serverPagination
  expandable
  virtualized
  stickyHeader
  renderEmptyState={...}
  renderLoadingState={...}
  onRowClick={...}
/>

En sentido técnico, es reutilizable. Si es mantenible es otra cuestión, y la respuesta honesta es “depende”. Cada bandera booleana duplica el número de combinaciones que debe soportar el componente, y cada nuevo consumidor que necesita una ligera variación añade otro parámetro en lugar de un componente adicional.

Una prueba más útil que “¿se puede reutilizar?” es determinar si la abstracción facilitará la creación de la próxima funcionalidad. Un componente con treinta parámetros a menudo no representa en absoluto una arquitectura reutilizable; es simplemente varios componentes distintos agrupados detrás de una única API. La composición, en la que los consumidores ensamblan piezas más pequeñas, suele ser la mejor solución, como se explica en cómo solucionar la sobrecarga de parámetros en React mediante composición y slots.

Dónde la carpeta utils oculta tu dominio

Casi todos los frontends de tamaño considerable desarrollan un directorio utils, y al principio eso no supone ningún problema. Con el tiempo, suele quedar así:

utils/
  formatDate.ts
  formatCurrency.ts
  userHelpers.ts
  permissions.ts
  productHelpers.ts
  validation.ts
  apiHelpers.ts
  stringUtils.ts
  common.ts
  helpers.ts

Una vez que la carpeta llega a este estado, nadie sabe dónde debe ir la nueva lógica, por lo que la siguiente función auxiliar se coloca en common.ts, y la siguiente también. El código está organizado por tipo de archivo, pero el dominio empresarial queda invisible. Las verificaciones de permisos, las reglas del producto y la integración con APIs se encuentran junto a la formateo de cadenas.

La mejor pregunta no es “¿en qué archivo de utilidades debe ir esto?”, sino “¿qué capacidad empresarial es responsable de esta lógica?”. Una estructura orientada a funcionalidades hace que esa respuesta sea explícita:

features/
  checkout/
    components/
    hooks/
    services/
    types/
    utils/

products/
    components/
    hooks/
    services/
    types/
payments/
    components/
    hooks/
    services/
    types/

Esto no es una orden de adoptar carpetas de características en todas partes. El principio subyacente es más importante que los nombres de las carpetas: el código debe agruparse según su propietario y responsabilidad, no simplemente según el tipo de archivo que sea.

Redux como almacén universal

Redux es una herramienta realmente útil, pero poner todo en el estado global es una de las formas más rápidas de generar deuda frontend. El almacén suele comenzar con fragmentos que claramente merecen ser globales:

auth
user
permissions
cart
products
checkout
notifications

Luego absorbe elementos que en realidad se refieren a una sola pantalla o interacción:

modal
filters
selectedTab
searchText
formState
temporaryUIState

Con el tiempo, la tienda funciona como una base de datos a nivel de toda la aplicación para cualquier cosa que dos componentes puedan querer leer. Técnicamente, casi cualquier valor puede ir a Redux, por lo que la pregunta correcta no es “¿se puede?”, sino si el estado realmente necesita ser global. Clasificar el estado en tres categorías ayuda.

Estado de la UI que debe permanecer local

Los valores de este tipo suelen pertenecer al componente que los utiliza, en useState o useReducer:

isModalOpen
activeTab
inputValue
hoveredItem
expandedRow

Al trasladarlos a una tienda global, cada modal abierta o fila sobre la que se pasa el cursor se convierte en un problema para toda la aplicación y se añade una capa de indirección a interacciones triviales.

Estado del servidor que necesita una estrategia de caché

Los datos propiedad del backend tienen necesidades diferentes:

products
orders
users
invoices

Aquí los problemas más difíciles son el caché, la deduplicación, la recarga y la invalidación. Estos requieren un enfoque deliberado para obtener datos (una biblioteca dedicada al estado del servidor o un patrón equivalente), en lugar de reductores escritos a mano que copian las respuestas al almacén. Las trade-offs se exploran con más detalle en reconsiderando el estado del servidor en aplicaciones grandes con React Query y Redux.

Estado que realmente abarca toda la aplicación

Algunas preocupaciones realmente afectan a todo el producto:

authentication
permissions
application-wide preferences
cross-feature workflows

Estos son candidatos razonables para un almacén global. La lección no es “evitar Redux”; es que la disponibilidad por sí sola no es motivo suficiente para hacer que el estado sea global.

Deuda en TypeScript: tipos que dejan de tener sentido

TypeScript es una de las herramientas más valiosas para grandes interfaces de usuario, pero no diseña la base de código por usted. Es totalmente posible distribuir este patrón en toda una aplicación y aún así considerar el proyecto como tipado:

const data: any = response.data;

Cada uso de any desactiva las verificaciones justo en el punto en que los datos ingresan a la aplicación, por lo que el compilador no puede advertirle cuando cambia la estructura de la API. Otro error común es declarar el mismo tipo de dominio de forma independiente en varios archivos:

type User = {
  id: string;
  name: string;
}

Si esto ocurre en varios equipos, se genera una serie de casi duplicados:

User
UserType
UserModel
UserResponse
UserDTO
UserData

Los desarrolladores invierten entonces más esfuerzo decidiendo qué tipo importar en lugar de beneficiarse del sistema de tipos. Buscar que “todo tenga un tipo” no cumple el objetivo real. El propósito es que los contratos importantes del sistema, como las respuestas de API, las entidades del dominio y las interfaces de componentes, sean explícitos y se definan una sola vez. Un buen TypeScript dificulta la representación de estados inválidos; un mal TypeScript solo genera más archivos. Una distinción útil es mantener un tipo de transporte para lo que devuelve la API y un tipo de dominio para lo con lo que trabaja la interfaz de usuario, convirtiéndolos en un solo lugar, en lugar de seis variantes poco relacionadas entre sí.

Cuando useEffect se convierte en la solución por defecto

Los efectos son otra fuente frecuente de problemas que se van acumulando con el tiempo. Comienzan con un único efecto que reacciona a un valor:

useEffect(() => {
  // do something
}, [something]);

Luego, un segundo efecto mantiene sincronizados dos valores:

useEffect(() => {
  // synchronize something
}, [a, b]);

Luego, existe un tercero principalmente para corregir el comportamiento de uno anterior:

useEffect(() => {
  // fix previous effect
}, [data]);

El componente se convierte en una cadena de efectos que desencadenan otros efectos, lo cual es difícil de seguir y propenso a renders adicionales y errores en el orden de ejecución. En esta etapa, ser bueno con React depende menos del conocimiento de la API y más de tener una visión clara de cómo fluyen los datos a través del componente.

Un ejemplo típico es almacenar datos derivados en el estado y sincronizarlos con un efecto:

const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Esto se renderiza una vez con una cadena vacía obsoleta, luego se ejecuta el efecto y finalmente se vuelve a renderizar. El valor podría simplemente calcularse durante el renderizado:

const fullName = `${firstName} ${lastName}`;

Parece una diferencia trivial, pero a lo largo de una aplicación estas pequeñas decisiones suman efecto. Los efectos sirven para sincronizarse con sistemas externos a React, como suscripciones, temporizadores o el DOM, y no para derivar un estado de otro. Un análisis más profundo se encuentra en dejar de sincronizar el estado con useEffect.

El límite servidor/cliente en Next.js

Next.js moderno añade una nueva dimensión a las decisiones arquitectónicas. Además de decidir qué componente renderiza algo, también hay que determinar dónde se ejecutará el código: en el servidor, en el cliente o en ambos. Ese límite en sí mismo es una elección arquitectónica.

Un error frecuente es convertir una gran parte del árbol en componentes cliente solo porque un elemento pequeño necesita interactividad. La directiva se coloca al principio del archivo de la página:

"use client";

Y ahora toda la página se envía al navegador como JavaScript del lado del cliente:

export default function ProductPage() {
  // entire page becomes client-side
}

Todo esto porque un solo botón necesita un estado local:

const [isOpen, setIsOpen] = useState(false);

Eso es conveniente, pero esa comodidad en un punto conlleva un costo en otro lugar: más JavaScript se envía al navegador, y la obtención de datos que podría haberse realizado en el servidor ahora debe hacerse en el cliente. En términos de App Router, todo lo importado desde un archivo "use client" pasa a formar parte del paquete del cliente, por lo que la ubicación de la directiva determina mucho más que el único componente que se pretendía hacer interactivo.

Es útil pensar en capas, llevando la interactividad lo más abajo posible en la estructura:

Server
   ↓
Data fetching
   ↓
Feature boundary
   ↓
Interactive client component
   ↓
Small local state

en lugar de usar un enfoque simplificado y plano:

Everything → Client Component

En la práctica, esto generalmente significa extraer el botón interactivo en su propio componente cliente pequeño y mantener la página, su carga de datos y la mayor parte de su marcado en el servidor.

La deuda estilística sigue siendo deuda

Los equipos a menudo consideran la deuda técnica como un problema relacionado con JavaScript y TypeScript. El estilo también acumula deuda de manera igualmente fiable, especialmente en productos de larga duración que combinan varios enfoques al mismo tiempo: CSS-in-JS y styled-components, módulos CSS, clases utilitarias, hojas de estilo globales antiguas y más de un sistema de diseño. Tarde o temprano, la biblioteca de componentes contendrá algo como esto:

Button
ButtonV2
PrimaryButton
NewButton
StyledButton
CommonButton
LegacyButton

Y nadie puede decir cuál es el botón correcto. La tecnología de estilo rara vez es la causa raíz. La verdadera deficiencia es la falta de responsabilidad en el sistema de diseño y de límites claros entre los componentes. Un frontend maduro tiene respuestas para preguntas como estas:

  • ¿Qué componentes forman parte del conjunto público y compartido?
  • ¿Cuáles existen solo para una función específica?
  • ¿Qué equipo es responsable de cada uno de ellos?
  • ¿Cuál es el método aprobado para agregar una variante?
  • ¿Cómo se implementan los cambios que rompen funcionalidades existentes?
  • ¿Qué cumple con los requisitos para ser incluido en el sistema de diseño?
  • ¿Qué debe permanecer local y nunca pasar a ser compartido?

Cuando la duplicación es mejor que la abstracción

A menudo, esta lección resulta difícil de aceptar para los desarrolladores experimentados. Supongamos que existen dos componentes que actualmente se parecen en un 90%:

ComponentA
ComponentB

El instinto es fusionarlos en uno solo:

<UniversalComponent />

No obstante, a veces la duplicación es la opción más económica, ya que aún no se sabe si ambos seguirán evolucionando en la misma dirección. Si divergen, el componente fusionado se llena de condicionales, y cualquier cambio en uno de los componentes que lo utilizan puede dañar al otro. Un poco de duplicación es saludable; una abstracción prematura puede generar un acoplamiento que dure años.

Una opción práctica por defecto: escríbelo dos veces, estudia el patrón y abstrae solo cuando un tercer caso de uso confirme su estructura. No es una ley universal, pero sí una regla de partida sensata.

Dependencias que ya no son necesarias

Los frontends grandes acumulan dependencias rápidamente. Una base de código de cinco años puede incluir fácilmente bibliotecas para:

  • estado, formularios y validación
  • fechas, gráficos y animaciones
  • kits de interfaz y estilos
  • obtención de datos
  • pruebas
  • herramientas de uso general
  • Algunas apenas se utilizan ya, y otras han sido reemplazadas por funciones del navegador o por capacidades integradas en el framework. Eliminar cualquiera de ellas parece arriesgado, por lo que permanecen, y el manifiesto se convierte gradualmente en un documento de arquitectura que nadie quiere leer:

    package.json
    

    La higiene de las dependencias no se refiere solo a aplicar parches de seguridad. Significa preguntarse periódicamente si cada dependencia sigue justificando su existencia, teniendo en cuenta el costo de mantenimiento, actualización y empaquetado que implica.

    El verdadero precio: cambios más lentos y arriesgados

    La deuda no es un problema porque a los ingenieros les resulte desagradable el código desordenado. Es un problema porque altera la economía detrás del desarrollo de software. En una aplicación saludable, una solicitud fluye a través de un proceso breve:

    Feature request
          ↓
    Small change
          ↓
    Tests
          ↓
    Deploy
    

    En una con alta deuda, la misma solicitud se convierte en una investigación extensa:

    Feature request
          ↓
    Find where this behavior lives
          ↓
    Understand 5-year-old code
          ↓
    Check Redux dependencies
          ↓
    Check shared component
          ↓
    Check side effects
          ↓
    Check styling overrides
          ↓
    Change code
          ↓
    Something else breaks
          ↓
    Fix regression
          ↓
    Add another workaround
    

    Cada paso de esa segunda cadena representa tiempo dedicado a comprender en lugar de a desarrollar, y los dos últimos pasos son los más reveladores: un cambio rompe algo no relacionado, y la solución añade otra solución temporal. Eso es cuando la base de código comienza a generar costos adicionales, y eso es lo que realmente significa la deuda técnica.

    Por qué “todavía funciona” es una señal engañosa

    Una aplicación puede funcionar sin problemas mientras tiene una enorme deuda técnica. Las páginas se cargan, los usuarios inician sesión, los pedidos se procesan, el panel de control se muestra correctamente y las pruebas continuas dan como resultado “verde”, por lo que todos asumen que el código está en buen estado. Luego llega una nueva función, y un cambio aparentemente sencillo lleva dos semanas de trabajo. Ese es el momento en que la deuda se vuelve visible.

    “¿Funciona la aplicación?”, por lo tanto, es una verificación de estado poco fiable. Una pregunta mucho más útil, especialmente a medida que los sistemas crecen, es: ¿cuánto cuesta cambiar esta aplicación de forma segura?

    Pagar la deuda sin reescribir

    La respuesta tentadora ante un código desordenado es reconstruirlo por completo. El resultado típico, seis meses después, es predecible:

    • la aplicación heredada sigue en funcionamiento en producción
    • el reemplazo está solo parcialmente terminado
    • ahora hay dos sistemas que necesitan mantenimiento
    • el equipo está agotado
    • el trabajo de desarrollo se ha estancado

    Las mejoras arquitectónicas graduales casi siempre funcionan mejor. Comienzan buscando dónde radica realmente el problema.

    Localizar las áreas costosas

    Resista la tentación de refactorizar todo. En su lugar, identifique las partes del producto con las que los desarrolladores tienen dificultades repetidamente, por ejemplo:

    Checkout
    User Management
    Search
    Dashboard
    Reporting
    

    Para cada candidato, busque estas señales:

    • Bugues que siguen apareciendo en la misma área
    • Archivos que la gente evita editar
    • Componentes que aparecen en casi todas las solicitudes de integración
    • Partes del producto que a los nuevos empleados les lleva más tiempo aprender
    • Funcionalidades cuyos cambios afectan módulos no relacionados

    Las áreas que surgen una y otra vez son sus puntos críticos de deuda técnica.

    Mida cuánto cuesta un cambio hoy

    No se necesita herramientaria sofisticada. Es suficiente hacer un seguimiento de unos pocos indicadores:

    Average time to implement feature
    Average PR size
    Number of files touched
    Regression frequency
    Build time
    Test execution time
    Deployment frequency
    

    Vincular la deuda con resultados de ingeniería como estos hace que sea mucho más fácil establecer prioridades, ya que se puede demostrar que un área específica ralentiza la entrega en lugar de argumentar que su código no es atractivo.

    Defina las reglas de dependencia antes de elegir un patrón

    Antes de debatir sobre la Arquitectura Limpia o el Diseño por Funcionalidades, resuelva una pregunta más básica: ¿qué puede depender de qué? Una dirección en capas simple podría verse así:

    app
     ↓
    features
     ↓
    shared
    

    Dentro de eso, una funcionalidad puede depender de bloques constructivos compartidos:

    features/product
          ↓
    shared/ui
    shared/lib
    

    pero el código compartido nunca debe influir en una funcionalidad específica:

    shared/ui
          X
    features/product
    

    La arquitectura específica puede cambiar con el tiempo; es la dirección de las dependencias lo que mantiene su coherencia. Una vez que la interfaz de usuario compartida comienza a importar elementos de una función del producto, deja de ser compartida y cada consumidor depende silenciosamente de esa función. Estas reglas también son fáciles de aplicar automáticamente con reglas de lint en las rutas de importación, lo que convierte una intención arquitectónica en algo que los sistemas CI verifican en cada solicitud de integración.

    Un diseño orientado a funciones para React y Next.js

    No existe una estructura única correcta para todos los proyectos, pero organizar por funciones proporciona límites naturales:

    src/
      app/
        routes/
        providers/
        layouts/
    
    
    features/
        checkout/
          components/
          hooks/
          services/
          state/
          types/
        products/
          components/
          hooks/
          services/
          state/
          types/
        users/
          components/
          hooks/
          services/
          state/
          types/
      shared/
        ui/
        hooks/
        lib/
        types/
    

    Copiar estos nombres de carpetas no es lo importante. Lo que realmente cuenta es que la propiedad se vuelva explícita: app conecta rutas, proveedores y diseños entre sí; cada funcionalidad posee sus propios componentes, ganchos, servicios, estado y tipos, mientras que shared contiene únicamente aquello que es verdaderamente genérico. Para una comparación de alternativas, consulte una comparación práctica de los patrones de estructura de carpetas en React.

    Hábitos que evitar en un frontend grande

    Al unirse o mantener una gran base de código en React, tenga cuidado con lo siguiente:

    • Globalizar el estado por defecto. La mayoría de los valores nunca necesitan salir de su componente.
    • Generalizar demasiado pronto. Primero entienda las variaciones y luego abstraiga.
  • Agregar un efecto como reflejo. Piense bien en el flujo de datos antes de sincronizar cualquier cosa.
  • Abrir otro utils.ts. Encuentre el dominio que debería albergar la lógica.
  • Marcar páginas completas como código del cliente. En App Router esto resulta especialmente costoso.
  • Instalar un paquete para algo que la plataforma o el framework ya hacen. Cada uno se convierte en una obligación de mantenimiento a largo plazo.
  • Refactorear antes de medir. La organización no es un resultado empresarial; lo que sí lo es es reducir el costo de los cambios.
  • De escribir mejor código a construir sistemas adaptables

    El cambio más significativo que trae la experiencia es un cambio en los objetivos, no una nueva técnica. Al principio, la pregunta natural es cómo escribir mejor un código determinado. Más tarde, la pregunta se vuelve cómo dejar el sistema más sencillo para quien lo modifique a continuación. Ese replanteamiento transforma la forma en que abordas los componentes y las abstracciones, la gestión de estado con Redux, TypeScript, Next.js, el estilo, las pruebas, la arquitectura general y las dependencias. En lugar de pulir archivos individuales, comienzas a optimizar la capacidad del sistema para evolucionar.

    La deuda técnica es una herramienta, no un enemigo

    No se puede eliminar la deuda técnica, y tratar de hacerlo es un error. Cada producto implica compromisos, y muchos de ellos generan deuda por diseño: entregar rápidamente, soportar varios requisitos empresariales al mismo tiempo, mantener funcionales navegadores obsoletos, cambiar la dirección del producto o integrar una aplicación adquirida. Incluso una excelente decisión arquitectónica puede convertirse en deuda cuando cambia el sistema circundante.

    Por lo tanto, “deuda cero” es un objetivo incorrecto. El objetivo realista es tener una deuda que el equipo comprenda, mantenga bajo control y pueda reducir cuando comience a afectar la entrega.

    Qué hace realmente que una aplicación grande sea difícil de trabajar

    Vale la pena ser preciso respecto a las causas raíz, porque rara vez son aquellas por las que la gente culpa:

    • No son muchos componentes los que representan el problema; lo son las relaciones entre ellos que nadie puede comprender.
  • El hecho de que ya exista Redux no es el problema; lo es que nadie sepa qué debe contener.
  • La gran cantidad de tipos en TypeScript no es el problema; lo son los tipos que ya no representan límites significativos.
  • El hecho de que Next.js ofrezca componentes server y client no es el problema; lo es un equipo que no sabe dónde está ese límite.
  • El código antiguo no es el problema; lo son las decisiones antiguas que ya no se pueden cambiar de forma segura.
  • En resumen, la deuda técnica no es código que se ve feo. Es código que hace que los cambios sean innecesariamente costosos, y la mejor arquitectura frontend no es aquella que parece perfecta desde el primer día, sino la que aún permite al equipo trabajar rápidamente cinco años después.

    Lista de verificación previa a una nueva funcionalidad

    Antes de añadir la próxima función a un códigobase de React, responda a estas preguntas:

    • ¿Existe una razón real para que este estado sea global?
    • ¿El backend es el propietario de estos datos, o se trata únicamente del estado de la interfaz?
    • ¿Está asumiendo el componente una segunda o tercera responsabilidad?
    • ¿La nueva abstracción resuelve una necesidad real, o solo hace que todo se vea más ordenado?
    • ¿Qué funcionalidad es la responsable de esta regla de negocio?
    • ¿Podría este código ejecutarse en el servidor de Next.js en su lugar?
    • ¿Es realmente necesario usar un Effect aquí?
    • ¿Añadirá esto otro paquete que haya que mantener?
    • ¿Se utiliza realmente el componente en diferentes funcionalidades?
    • ¿Podría un principiante entender este código sin tener que preguntar a quien lo escribió?

    Estas preguntas suelen valer más que otra lista más de buenas prácticas de React.

    Temas que vale la pena explorar a continuación

    Varios temas de esta guía merecen un análisis más profundo por separado:

    • Cuándo Redux es realmente la opción adecuada, en comparación con el estado local, Context, Redux Toolkit, RTK Query, manejo dedicado del estado del servidor, estado de URL y estado de formularios.
    • Refactorización de un componente de 1,000 líneas sin afectar la producción, como estudio de caso paso a paso.
    • Si los componentes reutilizables están empeorando la aplicación, abordando el problema de la explosión de propiedades, la composición y los sistemas de diseño.
    • Componentes del servidor frente a los componentes del cliente en Next.js, centrado en las decisiones de límites en lugar de en qué hace "use client".
    • TypeScript a gran escala, incluyendo any, generics, DTOs, tipos de dominio y API, duplicación e inferencia.
    • Qué debería reemplazar la carpeta utils como lugar para almacenar la lógica.
  • Saber cuándo una aplicación necesita una refactorización de arquitectura, idealmente con un modelo de puntuación basado en criterios como estos:
  • Change cost
    Bug frequency
    Dependency coupling
    Component size
    State complexity
    Build time
    Testability
    Developer onboarding
    

    Puntos clave

    • La deuda del frontend es la suma de decisiones pequeñas y razonables; observe la tendencia, no los commits individuales.
    • Juzgue la salud de la base de código por el costo y riesgo del cambio, no por si la aplicación funciona actualmente.
    • Mantenga el estado local por defecto, trate los datos del servidor como un problema de caché y globalice solo aquello que sea realmente transversal.
    • Prefiera los límites y la dirección de las dependencias sobre los nombres de carpetas o metodologías nombradas.
    • Pague la deuda de forma incremental en puntos críticos medidos, en lugar de apostar por una reescritura completa.

    Lecturas relacionadas