Flujo de decisiones para componentes
Un flujo de decisiones paso a paso para estructurar componentes de React. Recorre cada pregunta en orden: tus respuestas determinan la arquitectura, los patrones y las herramientas que necesitas antes de escribir una sola línea de código.
- ¿Qué renderiza este componente?
- ¿Necesita interactividad?
- ¿Qué estado posee?
- ¿De dónde vienen los datos?
- ¿Cómo se comunica con otros componentes?
- ¿Qué tipos de TypeScript necesita?
- ¿Cómo debe exportarse y organizarse?
- ¿Necesita optimización de rendimiento?
- ¿Maneja entradas del usuario (formularios)?
- ¿Cómo se probará?
- ¿Necesita accesibilidad?
- ¿Dónde vive en la estructura de archivos?
Cómo usar este flujo
Empieza en el Paso 1 y recorre cada decisión de forma secuencial. Cada paso se apoya en el anterior. Al final, tendrás un plano claro para tu componente: qué renderiza, cómo gestiona el estado, de dónde vienen los datos y cómo se comunica con el resto de tu aplicación.
Paso 1: ¿Qué renderiza este componente?
Determina primero la salida visual. Todo lo demás se deriva de lo que ve el usuario.
| Pregunta | Si es sí | Si es no |
|---|---|---|
| ¿Renderiza contenido estático (texto, imágenes, layout)? | Mantenlo simple: un Server Component sin hooks ni estado | Pasa al Paso 2 |
| ¿Renderiza una lista de elementos? | Necesitas .map(), props key únicas y probablemente datos de un padre o de una API | Considera un componente de un solo elemento |
| ¿Renderiza otros componentes (composición)? | Diseña ahora la API basada en children o en slots | Es un componente hoja: enfócate en las props |
| ¿A veces no renderiza nada (condicional)? | Planifica tu estrategia de renderizado condicional (ternario, &&, retorno temprano) | Siempre renderiza: camino más sencillo |
Decisión clave: Si el componente solo renderiza contenido estático sin interactividad, debe ser un Server Component (el valor por defecto en el App Router de Next.js). Detente aquí: no necesitas JavaScript del lado del cliente.
Relacionado: Componentes -- patrones de composición, props, children | JSX y TSX -- reglas de expresiones, fragments, compilación de JSX | Renderizado condicional -- patrones de ternario,
&&, retorno temprano | Server Components -- cuándo y por qué usarlos
Paso 2: ¿Necesita interactividad?
Si el usuario hace clic, escribe, pasa el cursor por encima, arrastra o interactúa de cualquier otra forma con él, es un Client Component.
| Tipo de interacción | Qué necesitas | Ejemplo |
|---|---|---|
| Clics de botón / toggles | Manejador onClick, posiblemente useState | Me gusta/quitar me gusta, expandir/colapsar |
| Entrada de texto | Input controlado con useState + onChange | Barra de búsqueda, campo de formulario |
| Efectos de hover / focus | Solo CSS (preferido) o onMouseEnter/onFocus | Tooltip, disparador de menú desplegable |
| Arrastrar y soltar | Librería de terceros (dnd-kit) o Drag API | Lista ordenable, tablero kanban |
| Atajos de teclado | useEffect + addEventListener o hook personalizado | Búsqueda con Cmd+K, Escape para cerrar |
Decisión clave: Añade "use client" al principio del archivo solo cuando el componente realmente necesite APIs del navegador, hooks o manejadores de eventos. Mantén el límite del cliente lo más bajo posible en el árbol.
Relacionado: Manejo de eventos -- eventos sintéticos, patrones de manejadores | Eventos de ratón -- clic, doble clic, hover | Eventos de teclado -- keydown, combinaciones de teclas | Eventos de foco -- focus, blur, navegación con tabulador | useKeyboardShortcut -- hook personalizado para combinaciones de teclas
Paso 3: ¿Qué estado posee?
Determina qué datos gestiona el componente internamente frente a los que vienen de fuera.
| Escenario de estado | Enfoque recomendado |
|---|---|
| Sin estado: pura visualización | Solo props. Los componentes sin estado son más fáciles de probar y reutilizar. |
| Toggle/contador simple (1-2 valores) | useState: una llamada por cada valor independiente |
| Campos relacionados que cambian juntos (formulario con más de 3 campos) | Un único useState con un objeto, useReducer para transiciones complejas, o un store de Zustand si el mismo estado puede necesitar leerse/escribirse desde componentes hermanos |
| Máquina de estados compleja (asistente de varios pasos, estado de arrastre) | useReducer con acciones explícitas, una librería de máquina de estados, o un store de Zustand con métodos de acción cuando los pasos abarcan varios componentes |
| Valores derivados (lista filtrada, total calculado) | Calcúlalos durante el render: no los almacenes en un estado aparte |
Decisión clave: Si estás recurriendo a más de 3-4 llamadas a useState en un mismo componente, detente y considera useReducer o extraer un hook personalizado.
Relacionado: useState -- funciones actualizadoras, inicialización perezosa, batching | useReducer -- estado basado en acciones para lógica compleja | Configuración de Zustand -- store ligero para estado entre componentes | Tipar el estado -- tipar formas de estado complejas | Máquinas de estados -- patrones de estado finito para la UI
Paso 4: ¿De dónde vienen los datos?
La estrategia de obtención de datos depende de si estás en un Server Component o en un Client Component.
| Fuente de datos | Mejor enfoque | Evita |
|---|---|---|
| Base de datos / ORM | Obtenerlos directamente en un Server Component asíncrono | Exponer consultas a la BD en código del cliente |
| Tu propia API | Server Action o fetch en un Server Component | useEffect + fetch en un Client Component (provoca cascadas) |
| API de terceros | Fetch en Server Component, o SWR/TanStack Query para el lado del cliente | useEffect + fetch sin caché |
| Parámetros de URL / search params | useParams() / useSearchParams() en un Client Component, o la prop params en un Server Component | Parsear window.location manualmente |
| Componente padre | Props: siempre el camino más sencillo | Estado global para la comunicación padre-hijo |
| Entrada del usuario | Estado controlado (useState + onChange) | Refs no controladas para valores que necesitas validar o mostrar |
Decisión clave: Prioriza los Server Components para la obtención de datos. Obtén datos en el cliente solo cuando necesites actualizaciones en tiempo real, refrescos iniciados por el usuario o scroll infinito.
Relacionado: Obtención básica con SWR -- obtención de datos en el cliente con caché | Revalidación con SWR -- estrategias stale-while-revalidate | useEffect -- cuándo debes obtener datos en el cliente | Obtención de datos en Next.js -- patrones de datos en Server Components | Tipar respuestas de API -- resultados de fetch con tipos seguros
Paso 5: ¿Cómo se comunica con otros componentes?
Determina cómo fluyen los datos y los eventos entre este componente y el resto de la aplicación.
| Patrón de comunicación | Cuándo usarlo | Ejemplo |
|---|---|---|
| Props hacia abajo | El padre pasa datos al hijo | <UserCard name={user.name} /> |
| Callbacks hacia arriba | El hijo notifica un evento al padre | <SearchInput onSearch={handleSearch} /> |
| Context | Muchos descendientes necesitan los mismos datos (tema, autenticación, locale) | useContext(ThemeContext) |
| Store global (Zustand) | Estado transversal compartido por componentes no relacionados | Cantidad del carrito en el header + página de checkout |
| Estado en la URL | Estado que debe sobrevivir a un refresco o poder compartirse | Filtros, paginación, pestaña activa |
| Server Actions | Envíos de formularios o mutaciones que llegan al servidor | <form action={submitForm}> |
Decisión clave: Empieza con props y callbacks. Recurre a Context cuando estés pasando props a través de más de 3 niveles. Recurre a Zustand cuando varios componentes no relacionados necesiten el mismo estado.
Relacionado: Composición -- slots y patrones de layout | Patrones de Context -- cuándo y cómo usar Context | Configuración de Zustand -- creación y uso del store | Context vs. Zustand -- elegir la herramienta adecuada | Server Actions -- mutaciones desde el cliente | Controlado vs. no controlado -- diseño de la API del componente
Paso 6: ¿Qué tipos de TypeScript necesita?
Define tus tipos antes de escribir el cuerpo del componente. Esto detecta errores de diseño de forma temprana.
| Qué tipar | Cómo |
|---|---|
| Interfaz de props | interface UserCardProps { name: string; email: string; onEdit?: () => void; } |
| Forma del estado | useState<User | null>(null) -- genérico explícito para tipos complejos |
| Manejadores de eventos | React.MouseEventHandler<HTMLButtonElement> o en línea (e: React.ChangeEvent<HTMLInputElement>) => void |
| Children | React.ReactNode para cualquier cosa renderizable, React.ReactElement para un único elemento |
| Ref | React.Ref<HTMLInputElement> para refs reenviadas |
| Uniones discriminadas | type ButtonProps = { variant: "link"; href: string } | { variant: "button"; onClick: () => void } |
Decisión clave: Tipa siempre las props como una interface (no en línea) cuando el componente tenga más de 2 props. Exporta la interfaz para que los consumidores puedan extenderla o referenciarla.
Relacionado: Tipar props -- interfaces, props opcionales, rest props | Tipar eventos -- tipos de manejadores de eventos | Tipar refs -- tipos de refs y reenvío | Uniones discriminadas -- props de variante con tipos seguros | Genéricos -- tipado de componentes genéricos | Fundamentos de TypeScript + React -- primeros pasos con TS en React
Paso 7: ¿Cómo debe exportarse y organizarse?
La estructura de archivos y el estilo de exportación afectan al tree-shaking, la claridad en las DevTools y las convenciones del equipo.
| Pregunta | Recomendación |
|---|---|
| ¿Es un archivo de página/layout/ruta de Next.js? | export default function PageName() -- declaración de función, siempre con nombre |
| ¿Es un componente compartido/reutilizable? | Exportación con nombre: export function UserCard() -- eliminable por tree-shaking, se permiten varios por archivo |
| ¿Es un helper puntual en el mismo archivo? | Declaración de función simple debajo de la exportación principal (el hoisting hace el orden flexible) |
| ¿Necesita propiedades estáticas? | Dos pasos: const Tabs = () => {...}; Tabs.Panel = TabPanel; export default Tabs; |
| ¿Función flecha o declaración de función? | Declaraciones de función para componentes (hoisted, genéricos limpios). Ajústate a las reglas de lint de tu equipo. |
Decisión clave: Sé coherente dentro de un archivo. Si el componente principal es una declaración de función, los helpers también deberían serlo. Mezclar estilos señala una diferencia intencional donde no la hay.
Relacionado: Sintaxis de componentes de función -- las 9 variaciones de sintaxis con sus compensaciones | Decisiones de arquitectura de React y Next.js -- opciones jerarquizadas para preguntas comunes de arquitectura
Paso 8: ¿Necesita optimización de rendimiento?
Optimiza solo cuando midas un problema. La optimización prematura añade complejidad sin ningún beneficio.
| Síntoma | Herramienta | Cuándo aplicarla |
|---|---|---|
| El hijo se vuelve a renderizar cuando cambia el estado del padre | React.memo() | Solo si el hijo es costoso de renderizar y las props no han cambiado |
| Cálculo costoso en cada render | useMemo | Solo si el cálculo tarda más de 1 ms y las entradas rara vez cambian |
| Un callback provoca el re-render del hijo | useCallback | Solo al pasar callbacks a hijos memoizados |
| Renderizado de listas grandes | Virtualización (react-window, tanstack-virtual) | Listas con más de 100 elementos visibles en el DOM |
| Componente pesado en la carga inicial | React.lazy() + Suspense, o next/dynamic | Modales, gráficos, editores: cualquier cosa no visible en el primer pintado |
| Desplazamiento de layout por imágenes | next/image con width/height o fill | Toda imagen debe declarar sus dimensiones |
Decisión clave: Perfila primero con el Profiler de React DevTools o la pestaña Performance de Chrome. Nunca añadas memo, useMemo o useCallback sin evidencia de un problema de rendimiento.
Relacionado: Patrones de rendimiento -- memoización, virtualización, división de código | useMemo -- cuándo ayuda la memoización (y cuándo perjudica) | useCallback -- referencias estables para props de hijos | React Compiler -- memoización automática en React 19 | Suspense -- carga perezosa y streaming
Paso 9: ¿Maneja entradas del usuario (formularios)?
Los formularios tienen su propio árbol de decisiones. La respuesta depende de la complejidad y de dónde ocurra la validación.
| Complejidad del formulario | Stack recomendado |
|---|---|
| 1-3 campos, envío simple | <form action={serverAction}> nativo + useActionState + useFormStatus |
| 4-8 campos con validación | react-hook-form + esquema zod + componentes de formulario de shadcn/ui |
| Asistente de varios pasos | react-hook-form + useReducer para el estado de los pasos + esquemas zod por paso |
| Subida de archivos | <input type="file"> + FormData + server action o URL prefirmada |
| Validación en tiempo real (a medida que se escribe) | react-hook-form en modo "onChange" + zod |
| Actualizaciones optimistas | Hook useOptimistic para dar respuesta instantánea mientras el servidor procesa |
Decisión clave: Para formularios simples, evita las librerías por completo: useActionState y useFormStatus, integrados en React 19, gestionan el estado pendiente, los errores y la mejora progresiva sin ninguna dependencia. Recurre a react-hook-form + zod cuando necesites validación compleja o muchos campos.
Relacionado: Formularios -- controlado vs. no controlado, envío de formularios | Checklist de decisiones para formularios -- 20 preguntas que hacer antes de construir un formulario | Checklist de decisiones para formularios con Gherkin -- planificación de formularios guiada por pruebas con BDD | React Hook Form + Zod -- formularios validados por esquema | Patrones de formulario (básico) -- recetas de formularios simples | Patrones de formulario (complejo) -- varios pasos y campos dinámicos | Controlado vs. no controlado -- cuándo usar cada uno | Eventos de formulario -- onChange, onSubmit, onBlur
Paso 10: ¿Cómo se probará?
Elige tu estrategia de pruebas según la complejidad y el riesgo del componente.
| Tipo de componente | Enfoque de pruebas |
|---|---|
| Pura visualización (sin estado) | Prueba de snapshot u omitirla: bajo riesgo, alta rotación |
| Interactivo (clics, entradas) | Prueba de integración con React Testing Library: simula acciones del usuario |
| Formulario con validación | Prueba el camino feliz y cada regla de validación: usa escenarios de Gherkin como especificaciones |
| Obtención de datos | Simula la API, prueba los estados de carga/error/éxito |
| Hook personalizado | Pruébalo con renderHook de React Testing Library |
| Flujo de trabajo complejo (varios pasos) | Prueba end-to-end con Playwright o Cypress |
Decisión clave: Prueba el comportamiento, no la implementación. Si haces aserciones sobre el estado interno o una estructura del DOM específica, la prueba se romperá en cada refactorización. Haz aserciones sobre lo que el usuario ve y hace.
Relacionado: Checklist de decisiones para formularios con Gherkin -- escribe especificaciones BDD antes de programar | De Gherkin a código -- traducir escenarios de Gherkin a código de pruebas | De Gherkin al pipeline de despliegue -- de la especificación al CI/CD
Paso 11: ¿Necesita accesibilidad?
La accesibilidad no es opcional: es un requisito básico. Planifícala en el componente desde el principio.
| Elemento | Requisitos mínimos |
|---|---|
| Botones | Usa <button>, no <div onClick>. Añade aria-label si es solo un icono. |
| Inputs | Asócialos con una <label> mediante htmlFor. Añade aria-describedby para los mensajes de error. |
| Modales / diálogos | Atrapa el foco, devuélvelo al cerrar. Usa role="dialog" y aria-modal="true". |
| Imágenes | Texto alt siempre. Las imágenes decorativas llevan alt="". |
| Navegación | Usa landmarks <nav>, <main>, <aside>. Asegúrate de que la navegación con teclado funcione. |
| Contenido dinámico | Usa regiones aria-live para contenido que se actualiza sin recargar la página. |
| Contraste de color | Mínimo 4.5:1 para texto normal, 3:1 para texto grande (WCAG AA). |
Decisión clave: Usa primero elementos HTML semánticos (<button>, <nav>, <dialog>). Recurre a atributos ARIA solo cuando la semántica nativa no cubra tu caso de uso.
Relacionado: Accesibilidad de formularios -- patrones de formularios accesibles y visualización de errores | Visualización de errores de formulario -- errores en línea, resumen, aria-live
Paso 12: ¿Dónde vive en la estructura de archivos?
Organiza por funcionalidad, no por tipo. Coloca juntos los archivos relacionados.
| Tipo de archivo | Ubicación | Ejemplo |
|---|---|---|
| Componente de página | app/[route]/page.tsx | app/dashboard/page.tsx |
| Layout | app/[route]/layout.tsx | app/dashboard/layout.tsx |
| Componente de UI compartido | components/[name].tsx | components/user-card.tsx |
| Componente específico de una funcionalidad | app/[feature]/components/[name].tsx | app/dashboard/components/stats-chart.tsx |
| Hook personalizado | hooks/[name].ts o junto a su componente | hooks/use-toggle.ts |
| Tipos | Colócalos en el mismo archivo, o types/[domain].ts para tipos compartidos | types/user.ts |
| Server Actions | app/[feature]/actions.ts | app/dashboard/actions.ts |
Decisión clave: Si un componente lo usa una sola página, colócalo junto a esa página. Si lo usan 2 o más páginas, promuévelo a components/. No crees carpetas para un solo archivo.
Relacionado: App Router de Next.js -- convenciones de archivos, páginas y segmentos de ruta | Layouts de Next.js -- layouts anidados, templates y grupos de rutas
Diagrama de flujo de referencia rápida
Inicio
|
v
1. ¿Qué renderiza? -----------> ¿Solo estático? --> Server Component (listo)
|
v
2. ¿Necesita interactividad? -> ¿No? --> Server Component con props (listo)
|
v (sí: añade "use client")
3. ¿Qué estado posee? -------> ¿Ninguno? --> Client Component sin estado
| ¿Simple? --> useState
| ¿Complejo? --> useReducer o hook personalizado
v
4. ¿De dónde vienen los datos? --> ¿Servidor? --> Obtenerlos en el Server Component padre y pasarlos como props
| ¿Cliente? --> SWR o TanStack Query
v
5. ¿Cómo se comunica? --------> ¿Padre-hijo? --> Props + callbacks
| ¿Transversal? --> Context o Zustand
v
6. Define los tipos de TypeScript
v
7. Elige el estilo de exportación
v
8. Optimiza solo si lo has medido
v
9. ¿Es un formulario? --------> ¿Simple? --> useActionState
| ¿Complejo? --> react-hook-form + zod
v
10. Planifica la estrategia de pruebas
v
11. Añade accesibilidad
v
12. Ubícalo en la estructura de archivos
v
Listo -- empieza a programar
Relacionado
- Fundamentos de React -- 15 ejemplos de componentes que cubren cada concepto de este flujo
- Decisiones de arquitectura de React y Next.js -- opciones jerarquizadas para decisiones comunes de arquitectura
- Checklist de decisiones para formularios -- 20 preguntas que hacer antes de construir cualquier formulario
- Mejores prácticas -- resumen de mejores prácticas de Fundamentos de React
- Guía de hooks personalizados -- cuándo y cómo extraer hooks personalizados