Files
inq-roi-simulador-web/sprints/sprint-5/ETAPA_4B_PROMPT.md
Marcos Benítez 4a19eda1b2
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
fix guardar perfil
2026-06-20 00:39:39 -03:00

4.7 KiB

Sprint 5 — Etapa 4B: Fix loop infinito al guardar perfil

Contexto

Etapa 4 completada con validación visual parcial. Al guardar cambios en el Sheet de "Editar perfil" (nombre o empresa), la app se congela indefinidamente. El resto de Etapa 4 está OK.

Diagnóstico

El síntoma (app congela, no responde) es loop infinito en React. La secuencia probable:

  1. Usuario guarda perfil → UPDATE users SET name=?, company=? WHERE id=?
  2. updateUser({ name, company }) actualiza el AuthUser en el store/context local
  3. Esa actualización de estado dispara un useEffect o el onAuthStateChange callback
  4. Ese callback re-lee la tabla users desde Supabase (SELECT company, name FROM users WHERE id=?)
  5. Al recibir el resultado, vuelve a llamar updateUser(...) con los mismos valores
  6. → Loop infinito

La raíz del problema es que el onAuthStateChange que se implementó en Etapa 4 para leer company y name de la DB se re-dispara cuando el store local se actualiza, o que hay un useEffect con dependencia en user que no tiene guard contra actualizaciones idempotentes.

Fix

Paso 1 — Diagnosticar dónde está el loop

Localizar el useEffect o callback que lee de la tabla users después de auth state change. Buscar en:

  • AuthContext.tsx o equivalente
  • El hook useAuth
  • Cualquier useEffect que tenga user o authUser como dependencia y haga una query a users

Paso 2 — Separar el flujo de "save perfil" del flujo de auth state change

El problema es que updateUser (actualización del store local) está acoplado al mecanismo de re-lectura de la DB. La solución es desacoplarlos:

Opción A — Flag de re-entrada (más simple):

Agregar un ref isUpdatingProfile que se activa durante el save y previene que el onAuthStateChange re-lea de la DB mientras está activo:

const isUpdatingProfile = useRef(false)

// En el save del perfil:
const saveProfile = async ({ name, company }) => {
  isUpdatingProfile.current = true
  try {
    await supabase.from('users').update({ name, company }).eq('id', user.id)
    // Actualizar store local directamente, sin pasar por onAuthStateChange:
    setUser(prev => prev ? { ...prev, name, company } : prev)
  } finally {
    isUpdatingProfile.current = false
  }
}

// En onAuthStateChange o el useEffect que re-lee la DB:
if (isUpdatingProfile.current) return  // ← guard de re-entrada

Opción B — Actualizar solo el estado local sin re-leer DB (más limpia):

En el save del perfil, no re-disparar el mecanismo de onAuthStateChange. Solo actualizar el estado local con los valores que el usuario acaba de escribir — ya los tenés, no hace falta volver a leerlos de la DB:

const saveProfile = async ({ name, company }) => {
  await supabase.from('users').update({ name, company }).eq('id', user.id)
  
  // Actualizar el AuthUser en el store directamente con los valores conocidos.
  // NO llamar a ninguna función que re-lea de la DB.
  setCurrentUser(prev => prev ? { ...prev, name, company } : prev)
  
  // Cerrar el Sheet.
  setProfileSheetOpen(false)
}

Para que esto funcione, setCurrentUser (o equivalente) debe ser una actualización directa del estado, sin side effects que disparen re-lectura.

Recomendación: Opción B. Es más predecible: el save actualiza la DB y el estado local con los valores exactos que el usuario confirmó. No hay queries adicionales, no hay riesgo de loop.

Paso 3 — Verificar que el useEffect que lee de users tiene guards adecuados

El useEffect o callback que lee company y name de la tabla users al detectar un cambio de auth debería ejecutarse solo en eventos genuinos de auth (login, token refresh), no en cada actualización del objeto AuthUser local.

Verificar que la dependencia del efecto es el ID de sesión o el evento de auth, no el objeto user completo (que cambia cada vez que lo actualizamos localmente):

// ❌ Loop: se re-ejecuta cada vez que user cambia, incluyendo el update local:
useEffect(() => {
  if (user) fetchCompanyFromDB(user.id)
}, [user])

// ✅ Correcto: solo se ejecuta cuando cambia la sesión real:
useEffect(() => {
  if (sessionId) fetchCompanyFromDB(userId)
}, [sessionId])  // sessionId es estable entre updates locales

Criterios de validación

  • Guardar nombre en el Sheet → app no se congela → nombre actualizado en el dropdown
  • Guardar empresa en el Sheet → app no se congela → empresa aparece en el dropdown
  • Cmd+R después de guardar → los valores persisten (están en Supabase)
  • Navegar a otra página y volver → los valores siguen correctos
  • npm run build limpio
  • npm run test — 528+ Vitest verdes
  • Validación visual del director antes del OK formal