[bug] Error de hidratación por sesión y tema leídos del navegador; parpadeo del tema oscuro #25

Open
opened 2026-10-02 19:22:00 +00:00 by Carlos · 0 comments
Member

Qué pasa

La sesión del usuario y el tema se leen del almacenamiento del navegador al inicializar el estado, y eso produce un render distinto en el servidor y en el cliente:

  • apps/web/src/app/_core/hooks/use-session-user.ts:15 hace useState(readSessionUser). En el servidor no hay window y el valor es null; en el cliente es el usuario guardado.
  • AdminShell y PosShell renderizan null sin usuario, así que el HTML del servidor está vacío y el cliente renderiza el layout completo. React lo detecta como error de hidratación y vuelve a renderizar todo.
  • apps/web/src/app/_core/hooks/use-theme.tsx:34 tiene el mismo patrón con localStorage.
  • readSessionUser hace JSON.parse sin try: si el valor guardado está corrupto, la app truena en vez de mandar al login.
  • El tema se aplica en un useLayoutEffect después de hidratar, sin script inline en el <head>, así que con tema oscuro cada carga muestra primero la página en claro.

Entorno

local

Módulos

Web / frontend, Auth / sesión / roles

Severidad

Cosmética / menor

Pasos para reproducir

  1. Iniciar sesión y entrar a /admin o /pos.
  2. Recargar la página con la consola del navegador abierta (o en npm run dev con el overlay de errores de Next).
  3. Elegir el tema oscuro y recargar.
  4. En la consola, ejecutar sessionStorage.setItem("pdv:user", "{") y recargar.

Esperado

  • Sin errores de hidratación.
  • Con tema oscuro, la página carga oscura desde el primer cuadro.
  • Con una sesión corrupta, la app manda al login.

Actual

  • Error de hidratación en cada carga de las pantallas con sesión.
  • Destello en tema claro antes de aplicar el oscuro.
  • Con una sesión corrupta, la app truena con SyntaxError.

Evidencia

Encontrado en auditoría de código; no reproducido en ejecución.

// use-session-user.ts
function readSessionUser(): AuthUser | null {
  if (typeof window === "undefined") return null;
  const raw = sessionStorage.getItem("pdv:user");
  return raw ? (JSON.parse(raw) as AuthUser) : null;   // sin try
}
const [user, setUser] = useState<AuthUser | null>(readSessionUser);   // distinto en servidor y cliente

Arreglo sugerido: leer la sesión con useSyncExternalStore (con getServerSnapshot que devuelva null) o después de montar, mostrando un estado de carga en vez de null; envolver el JSON.parse en try; y aplicar el tema con un script inline en el <head> antes de hidratar. Con #5 la sesión pasaría a una cookie y el servidor ya la conocería.

¿Está también en producción?

  • Sí — este issue no basta; hay que abrir un Incidente / hotfix
  • No — solo local / staging
  • No sé

Antes de abrir

  • Busqué un issue duplicado
### Qué pasa La sesión del usuario y el tema se leen del almacenamiento del navegador al inicializar el estado, y eso produce un render distinto en el servidor y en el cliente: - `apps/web/src/app/_core/hooks/use-session-user.ts:15` hace `useState(readSessionUser)`. En el servidor no hay `window` y el valor es `null`; en el cliente es el usuario guardado. - `AdminShell` y `PosShell` renderizan `null` sin usuario, así que el HTML del servidor está vacío y el cliente renderiza el layout completo. React lo detecta como error de hidratación y vuelve a renderizar todo. - `apps/web/src/app/_core/hooks/use-theme.tsx:34` tiene el mismo patrón con `localStorage`. - `readSessionUser` hace `JSON.parse` sin `try`: si el valor guardado está corrupto, la app truena en vez de mandar al login. - El tema se aplica en un `useLayoutEffect` después de hidratar, sin script inline en el `<head>`, así que con tema oscuro cada carga muestra primero la página en claro. ### Entorno local ### Módulos Web / frontend, Auth / sesión / roles ### Severidad Cosmética / menor ### Pasos para reproducir 1. Iniciar sesión y entrar a `/admin` o `/pos`. 2. Recargar la página con la consola del navegador abierta (o en `npm run dev` con el overlay de errores de Next). 3. Elegir el tema oscuro y recargar. 4. En la consola, ejecutar `sessionStorage.setItem("pdv:user", "{")` y recargar. ### Esperado - Sin errores de hidratación. - Con tema oscuro, la página carga oscura desde el primer cuadro. - Con una sesión corrupta, la app manda al login. ### Actual - Error de hidratación en cada carga de las pantallas con sesión. - Destello en tema claro antes de aplicar el oscuro. - Con una sesión corrupta, la app truena con `SyntaxError`. ### Evidencia ```shell Encontrado en auditoría de código; no reproducido en ejecución. // use-session-user.ts function readSessionUser(): AuthUser | null { if (typeof window === "undefined") return null; const raw = sessionStorage.getItem("pdv:user"); return raw ? (JSON.parse(raw) as AuthUser) : null; // sin try } const [user, setUser] = useState<AuthUser | null>(readSessionUser); // distinto en servidor y cliente ``` Arreglo sugerido: leer la sesión con `useSyncExternalStore` (con `getServerSnapshot` que devuelva `null`) o después de montar, mostrando un estado de carga en vez de `null`; envolver el `JSON.parse` en `try`; y aplicar el tema con un script inline en el `<head>` antes de hidratar. Con #5 la sesión pasaría a una cookie y el servidor ya la conocería. ### ¿Está también en producción? - [ ] Sí — este issue no basta; hay que abrir un Incidente / hotfix - [ ] No — solo local / staging - [x] No sé ### Antes de abrir - [x] Busqué un issue duplicado
Emmanuel was assigned by Carlos 2026-10-02 20:03:24 +00:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RynextTechnologies/PDV#25