Patrones
Patrones probados en producción. Si vas a crear algo similar, partir de aquí.
Theme toggle con mounted-flag
Sección titulada «Theme toggle con mounted-flag»Problema: Cualquier UI que lee preferencias del usuario (theme, locale, etc.) en el cliente provoca hydration mismatch si el server renderiza un valor y el cliente otro.
Solución: flag mounted que delay el render hasta después del primer render del cliente.
'use client';import { useEffect, useState } from 'react';import { useTheme } from '@/components/theme/useTheme';
export function ThemeToggle() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []);
if (!mounted) return <div className="size-9" />; // placeholder mismo tamaño
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>...</button>;}Reglas:
- El placeholder debe tener el mismo tamaño que el componente real para evitar layout shift.
- Aplica a cualquier UI que lea theme, locale, timezone u otra preferencia del cliente.
Theme con cookie + SSR
Sección titulada «Theme con cookie + SSR»El theme se persiste en cookie + localStorage para que el server pueda renderizar el modo correcto desde el primer byte:
- Cookie (
theme) — leída porlayout.tsxserver-side, aplicada como classNamedarken<html>. - localStorage — backup para que el cliente reaplique sin esperar la cookie.
ThemeScript— script inline en<head>que aplica la clase antes de que React monte, evitando flash blanco en dark mode.
Archivos involucrados:
src/lib/theme-cookie.ts— parseo + nombre de cookie.src/components/theme/ThemeProvider.tsx— Context Provider.src/components/theme/ThemeScript.tsx— script anti-flash.src/components/theme/useTheme.ts— hook consumidor.
SessionProvider + useSession
Sección titulada «SessionProvider + useSession»Problema: Leer la sesión server-side en componentes globales (ej: Header) en una app con páginas mixtas (algunas estáticas, algunas dinámicas) rompe la UI: el Header server-rendered se desincroniza con el estado real del usuario en navegación client-side.
Solución: Usar SessionProvider (auth.js) en el root y useSession() en componentes cliente.
'use client';import { SessionProvider } from 'next-auth/react';
export function Providers({ children }) { return <SessionProvider>{children}</SessionProvider>;}
// components/layout/Header.tsx'use client';import { useSession } from 'next-auth/react';
export function Header() { const { data: session, status } = useSession(); if (status === 'loading') return <HeaderSkeleton />; return session ? <UserMenu user={session.user} /> : <LoginButton />;}Cuándo SÍ leer sesión server-side:
- Páginas individuales que necesitan la sesión para fetchear datos (ej:
/cards/[id]paracurrentUserId). - API routes / route handlers.
Cuándo NO:
- Components globales montados en
layout.tsx(Header, Footer, Sidebar global).
Server-only guards
Sección titulada «Server-only guards»Problema: Si por accidente importas código server-only (env vars secretas, JWT signing, API client interno) desde un Client Component, el secret se filtra al bundle del browser.
Solución: import 'server-only'; al tope de archivos sensibles.
import 'server-only';
export const env = { MONGODB_URI: process.env.MONGODB_URI!, JWT_SECRET: process.env.JWT_SECRET!, // ...};Archivos que deben tener server-only:
src/lib/env.tssrc/lib/jwt.tssrc/lib/api/client.ts(cliente HTTP interno)- Cualquier helper que toque secrets
Comportamiento: Si un Client Component importa uno de estos, el build falla. Mejor fallar en build que filtrar en producción.
Cache pattern para reads de marketplace
Sección titulada «Cache pattern para reads de marketplace»Reglas de cache en src/lib/api/*:
| Tipo de dato | Estrategia |
|---|---|
| Stock-dependent (listings, cards detail, precios) | cache: 'no-store' (siempre fresco) |
| Catálogo estático (sets, rarities, TCGs) | next: { revalidate: 300 } (5 min) |
| Endpoints muy estables (lista de rarezas) | next: { revalidate: 3600 } (1 h) |
Razón: los precios y stock cambian con cada publicación/edición/eliminación de listing. Cachearlos rompe la UX core del marketplace. Los sets y rarezas solo cambian con el cron diario, así que 5 min está bien.
Ver src/lib/api/catalog.ts y src/lib/api/listings.ts para implementación.
Precios en centavos
Sección titulada «Precios en centavos»Regla: El backend almacena precios en CLP cents (Math.round(precio * 100)).
En el frontend:
- Para mostrar texto al usuario: usar
formatPriceCents(cents)desrc/lib/format.ts. - Para meter en JSON-LD numérico:
Math.round(cents / 100)(Google espera pesos enteros). - Nunca dividir manualmente en el JSX.
import { formatPriceCents } from '@/lib/format';
<span>{formatPriceCents(card.minPrice)}</span> // "Desde $12.500"Favicon RGBA
Sección titulada «Favicon RGBA»Turbopack (loader de ICO) rechaza PNGs en modo RGB. Siempre exportar favicons en RGBA.
Si vas a regenerar el favicon, abrirlo en Photoshop/Affinity y asegurarse de que el modo sea RGBA antes de exportar.
Atlas migration → redeploy Cloud Run
Sección titulada «Atlas migration → redeploy Cloud Run»Tras un upgrade o migración del cluster Mongo Atlas, redeploy obligatorio de Cloud Run para refrescar los connection pools. El endpoint /health puede mentir y reportar OK aun con pools rotos. Pattern: gcloud run deploy --region us-central1 --project <project> ....
Documentado por completo en runbooks. Mencionado aquí porque afecta al frontend (si la API queda mal, /cards y /listings empiezan a dar 500).