Revisión React · PR #47 — feat: dashboard de leads con filtros dinámicos

CultivaFlow SaaS · Next.js 14 App Router · TypeScript · Tailwind CSS
2 críticos 4 altos 3 medios Revisado: 18 jun 2026 Revisor: react-reviewer v1.0
🚫

BLOQUEADO — No se puede mergear

Se han encontrado 2 issues CRITICAL y 4 HIGH. Resolución obligatoria antes de aprobación. Los 3 MEDIUM pueden corregirse en tickets de seguimiento.

2 Critical
4 High
3 Medium
2 Archivos revisados

⚠ CONFIG — eslint-plugin-react-hooks no detectado en .eslintrc.cjs

El proyecto no tiene configurada la regla react-hooks/rules-of-hooks ni react-hooks/exhaustive-deps. Muchos de los errores de hooks encontrados habrían sido capturados en CI antes de llegar a revisión. Instalar con npm install -D eslint-plugin-react-hooks y añadir al config.

Checklist previo al merge
Critical 2 issues — bloquean el merge
Critical Server Action sin validación de input
Archivo src/app/dashboard/leads/actions.ts:14
Issue La Server Action saveLeadAction acepta FormData sin ningún esquema de validación (zod/yup/valibot) antes de insertarlo en la base de datos.
// ❌ CRÍTICO — src/app/dashboard/leads/actions.ts:14 "use server" export async function saveLeadAction(formData: FormData) { // Sin auth check, sin validación const email = formData.get('email') as string const name = formData.get('name') as string await db.leads.insert({ email, name, createdAt: new Date() }) }
Impacto Las Server Actions son endpoints HTTP públicos. Sin validación, cualquier actor puede insertar leads con datos arbitrarios (SQL injection via ORM params, oversized payloads, spam).
Fix Añadir validación zod + verificación de sesión antes de operar en DB. Ver snippet correcto abajo.
// ✅ FIX — validación zod + auth check import { z } from 'zod' import { getServerSession } from 'next-auth/next' const LeadSchema = z.object({ email: z.string().email().max(254), name: z.string().min(1).max(120), }) export async function saveLeadAction(formData: FormData) { const session = await getServerSession() if (!session) throw new Error('Unauthorized') const parsed = LeadSchema.safeParse({ email: formData.get('email'), name: formData.get('name'), }) if (!parsed.success) return { error: parsed.error.flatten() } await db.leads.insert({ ...parsed.data, createdAt: new Date() }) }
Critical Import de Prisma Client en componente "use client"
Archivo src/app/dashboard/leads/LeadsDashboard.tsx:3
Issue El componente está marcado "use client" pero importa directamente @/lib/db (wrapper de Prisma Client). El módulo accede a DATABASE_URL en el bundle del cliente.
// ❌ CRÍTICO — LeadsDashboard.tsx:1-5 "use client" import { db } from '@/lib/db' // ← Prisma client importado en bundle cliente import { useState, useEffect } from 'react'
Impacto La cadena de conexión de la base de datos se expondrá en el JavaScript público. Cualquier usuario puede inspeccionarla en las DevTools. Además, Prisma no puede ejecutarse en un entorno Edge/Browser.
Fix Eliminar el import de db del Client Component. Los datos deben venir de una Route Handler o de props pasadas desde un Server Component padre. Añadir server-only al módulo db para prevenir re-ocurrencias.
// ✅ FIX — separar fetching en Server Component // src/app/dashboard/leads/page.tsx (Server Component) import { db } from '@/lib/db' import LeadsDashboard from './LeadsDashboard' export default async function LeadsPage() { const leads = await db.leads.findMany({ orderBy: { createdAt: 'desc' } }) return <LeadsDashboard initialLeads={leads} /> } // En lib/db.ts — añadir al inicio: import 'server-only'
High 4 issues — bloquean el merge
High useEffect con dependencia faltante — stale closure
Archivo src/app/dashboard/leads/LeadsDashboard.tsx:38
Issue fetchLeads usa filterState pero el array de deps está vacío ([]). Los filtros no actualizan la tabla al cambiar.
// ❌ HIGH — deps vacías, filterState no reactivo useEffect(() => { fetchLeads(filterState) // captura la versión inicial de filterState }, []) // ← falta filterState, fetchLeads // ✅ FIX useEffect(() => { const controller = new AbortController() fetchLeads(filterState, controller.signal) return () => controller.abort() }, [filterState, fetchLeads])
High Fetch sin cleanup — fuga de memoria en desmontaje
Archivo src/app/dashboard/leads/LeadsDashboard.tsx:42
Issue La función fetchLeads hace un fetch sin AbortController. Si el componente se desmonta antes de que la promesa resuelva, se producirá un setState on unmounted component y una fuga de memoria.
High Elemento interactivo sin accesibilidad de teclado
Archivo src/app/dashboard/leads/LeadsDashboard.tsx:87
Issue El botón de eliminar lead usa <div onClick> sin rol, sin tabIndex y sin handler de teclado. Inaccesible para usuarios de teclado y screen readers.
// ❌ HIGH — a11y <div onClick={() => deleteLead(id)} className="cursor-pointer"> Eliminar </div> // ✅ FIX — elemento semántico <button onClick={() => deleteLead(id)} aria-label="Eliminar lead" className="..." > Eliminar </button>
High key={index} en lista dinámica de leads
Archivo src/app/dashboard/leads/LeadsDashboard.tsx:105
Issue La lista usa key={index}. Al filtrar, ordenar o eliminar leads, React asociará el estado del componente de fila a la posición, no al elemento, causando bugs visuales.
// ❌ HIGH — key inestable {leads.map((lead, index) => ( <LeadRow key={index} lead={lead} /> ))} // ✅ FIX — usar el ID de la base de datos {leads.map((lead) => ( <LeadRow key={lead.id} lead={lead} /> ))}
Medium 3 issues — corregir en tickets de seguimiento
Medium Lista de 500+ filas sin virtualización
Archivo src/app/dashboard/leads/LeadsDashboard.tsx:100
Issue El componente renderiza todos los leads en el DOM simultáneamente. Con el volumen previsto (>500 filas), el scroll será lento y el tiempo de montaje considerable.
Fix Implementar virtualización con @tanstack/react-virtual o paginación server-side. Evaluar con React DevTools Profiler antes de decidir.
Medium Formulario sin elemento semántico <form>
Archivo src/app/dashboard/leads/QuickCaptureForm.tsx:12
Issue El formulario de captura está construido con <div> en lugar de <form>. Pierde submit-on-Enter nativo, integración con gestores de contraseñas y la semántica del árbol de accesibilidad.
Medium Validación manual reinventada en formulario no trivial
Archivo src/app/dashboard/leads/QuickCaptureForm.tsx:45
Issue La validación de email, nombre y empresa se hace con regex casero y múltiples useState de error por campo. El mismo esquema zod del Server Action puede reutilizarse en cliente con React Hook Form.
Fix Usar React Hook Form con @hookform/resolvers/zod para compartir el mismo LeadSchema cliente/servidor.
Alcance del revisor — división con typescript-reviewer
Área de revisión Responsable Estado en este PR
Hooks (reglas, deps, cleanup) react-reviewer ✓ 2 issues HIGH
Server/Client boundary, RSC leaks react-reviewer ✓ 1 issue CRITICAL
Server Actions (validación, auth) react-reviewer ✓ 1 issue CRITICAL
Accesibilidad (a11y, ARIA, semántica) react-reviewer ✓ 1 issue HIGH
Rendimiento (key, virtualización, memo) react-reviewer ✓ 2 issues MEDIUM + 1 HIGH
Tipado TypeScript, any/as, strict-null typescript-reviewer → No revisado aquí
Async/await, unhandled rejections typescript-reviewer → No revisado aquí
XSS genérico via innerHTML, Node.js sec typescript-reviewer → No revisado aquí