# 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: ```ts 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: ```ts 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): ```ts // ❌ 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