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
Iniciar sesión y entrar a /admin o /pos.
Recargar la página con la consola del navegador abierta (o en npm run dev con el overlay de errores de Next).
Elegir el tema oscuro y recargar.
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 Carlos2026-10-02 20:03:24 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:15haceuseState(readSessionUser). En el servidor no haywindowy el valor esnull; en el cliente es el usuario guardado.AdminShellyPosShellrenderizannullsin 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:34tiene el mismo patrón conlocalStorage.readSessionUserhaceJSON.parsesintry: si el valor guardado está corrupto, la app truena en vez de mandar al login.useLayoutEffectdespué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
/admino/pos.npm run devcon el overlay de errores de Next).sessionStorage.setItem("pdv:user", "{")y recargar.Esperado
Actual
SyntaxError.Evidencia
Arreglo sugerido: leer la sesión con
useSyncExternalStore(congetServerSnapshotque devuelvanull) o después de montar, mostrando un estado de carga en vez denull; envolver elJSON.parseentry; 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?
Antes de abrir