Ir al contenido principal
Ilustración: componentes de interfaz organizados en un árbol, desde la página hasta los botones y campos

El desarrollo basado en componentes invierte el enfoque tradicional, de arriba abajo, para construir interfaces de usuario. En lugar de construir páginas y extraer después las partes reutilizables, se empieza por componentes individuales y se componen para formar vistas.

La recompensa no es solo la reutilización. Cuando un componente se construye y se ejercita por sí solo, sus casos límite salen a la luz pronto, su API tiene que ser explícita y la página que finalmente lo usa es, sobre todo, ensamblaje. El coste es pensar un poco más al principio en los límites, algo que este artículo intenta abaratar.

La jerarquía de componentes

Organice los componentes en una jerarquía clara: átomos (botones, campos, etiquetas), moléculas (barras de búsqueda, grupos de formulario), organismos (navegación, secciones hero), plantillas (layouts de página) y páginas (instancias concretas). Esta metodología de atomic design, popularizada por Brad Frost, proporciona un vocabulario común para hablar de la arquitectura de la UI.

Trate la jerarquía como un vocabulario, no como una estructura de carpetas que deba obedecer. Los equipos a menudo se atascan discutiendo si una tarjeta es una molécula o un organismo. Una división más útil para el código es:

  • Primitivas que no saben nada de su dominio: Button, Input, Badge, Dialog.
  • Composiciones que organizan primitivas en un patrón reconocible: SearchField, StatCard, EmptyState.
  • Funcionalidades que conectan composiciones con datos y comportamiento reales: InvoiceTable, CheckoutSummary.

La regla que mantiene todo esto sano es que las dependencias solo apuntan hacia abajo. Una primitiva nunca importa una funcionalidad, y una composición nunca obtiene datos.

Desarrollo en aislamiento

Construya y pruebe los componentes fuera del contexto de una página completa. Herramientas como Storybook, o la Container API de Astro para renderizar un componente por sí solo, le permiten renderizar componentes con distintas props, estados y variaciones de datos. Así se detectan los casos límite pronto y se obtienen componentes más robustos.

El aislamiento le obliga a responder preguntas que una página completa le permite ignorar. ¿Cómo se ve la tabla con cero filas? ¿Con una fila cuyo nombre tiene 80 caracteres? ¿Mientras carga? ¿Después de que falle la solicitud? Anote cada uno de esos casos como un estado con nombre y renderícelo:

export const states = {
  loading: { status: 'loading' },
  empty:   { status: 'ready', rows: [] },
  error:   { status: 'error', message: 'Could not load invoices.' },
  long:    { status: 'ready', rows: [longNameRow, ...sampleRows] },
};

El desarrollo aislado también hace viable el testing de regresión visual. Capture snapshots de los estados de su componente y detecte automáticamente los cambios visuales no intencionados entre versiones. Mantenga los snapshots acotados (un estado de componente por imagen) para que un diff fallido señale directamente la causa en lugar de una página entera.

Props y flujo de datos

Diseñe APIs de componentes claras con props bien definidas. Use interfaces de TypeScript para documentar las entradas esperadas y proporcione valores por defecto razonables. Prefiera pasar props de forma explícita antes que usar context o estado global en la mayoría de los componentes: hace que el flujo de datos sea rastreable y que los componentes sean más fáciles de probar.

Algunos hábitos producen APIs que envejecen bien:

  • Modele estados, no booleanos. isLoading, isError e isEmpty como flags separados permiten combinaciones imposibles. Una única unión status, no.
  • Nombre las variantes por su intención. variant="danger" sobrevive a un cambio de marca; color="red", no.
  • Acepte children o slots para el contenido. Un componente que recibe las props title, subtitle, icon y footerText seguirá creciendo; uno que acepta contenido compuesto, no.
  • Transmita los atributos nativos. Un Button debería aceptar type, disabled y aria-* sin tener que volver a declarar cada uno.
type Status = 'loading' | 'empty' | 'error' | 'ready';

interface StatCardProps {
  /** Etiqueta breve que se muestra sobre el valor. */
  label: string;
  /** Valor formateado; se ignora salvo que status sea 'ready'. */
  value?: string;
  status: Status;
}

El context y los stores globales siguen teniendo su lugar: el tema, el idioma, el usuario actual. Esos datos son realmente ambientales. La mayoría de los demás datos deberían llegar a través de props, porque un componente cuyas entradas son todas visibles en su firma puede renderizarse en cualquier lugar, incluido un test.

Testing de componentes

Las pruebas unitarias verifican que los componentes se renderizan correctamente con las props dadas y que gestionan las interacciones del usuario como se espera. Las pruebas de integración validan que los componentes compuestos funcionan bien juntos. Aplique el principio rector de Testing Library: pruebe el comportamiento, no los detalles de implementación.

En la práctica, eso significa consultar por lo que percibe el usuario (rol, nombre accesible, texto visible) en lugar de por nombres de clase o estado interno:

test('shows the error message and a retry button', async () => {
  render(InvoiceTable, { status: 'error', message: 'Could not load invoices.' });
  expect(screen.getByText('Could not load invoices.')).toBeVisible();
  expect(screen.getByRole('button', { name: 'Retry' })).toBeEnabled();
});

Un test escrito así sobrevive a una refactorización del markup y además sirve como una comprobación ligera de accesibilidad: si usted no puede encontrar el botón por su rol y nombre, un lector de pantalla tampoco. La lógica pura, como la función que asigna una variante a nombres de clase, es más fácil de probar directamente como una función simple, sin ningún renderizado.

Documentar mientras construye

El aislamiento le da la documentación casi gratis, siempre que la capture mientras las decisiones están frescas. Para cada componente, registre:

  • Para qué sirve y, igual de importante, para qué no sirve.
  • Cada prop, con su tipo, su valor por defecto y una descripción de una línea.
  • Los estados que admite y cómo se ve cada uno.
  • El comportamiento con teclado y cualquier ARIA que gestione en nombre de quien lo consume.

Ponga las descripciones en las propias definiciones de tipos, como en el ejemplo de StatCardProps anterior, para que los editores las muestren al pasar el cursor y no puedan desviarse del código.

Dónde falla

El desarrollo basado en componentes tiene modos de fallo que conviene nombrar:

  • Abstracción prematura. Extraiga un componente compartido cuando tenga dos o tres usos reales, no un único uso futuro imaginado.
  • Explosión de props. Cuando un componente tiene más props booleanas que líneas de markup, divídalo en piezas más pequeñas.
  • Aislamiento sin integración. Cada componente puede verse perfecto en su propia story mientras la página sigue rota. Mantenga también un pequeño número de pruebas a nivel de página.

Conclusión

El desarrollo basado en componentes produce interfaces más consistentes, más fáciles de mantener y más rápidas de construir. Empiece por los bloques más pequeños, dé a cada uno una API explícita y un conjunto completo de estados, y vaya componiendo hacia arriba.

Compartir este artículo
design-systems frontend testing