Ir al contenido

Patrones

Patrones probados en producción. Si vas a crear algo similar, partir de aquí.

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.

El theme se persiste en cookie + localStorage para que el server pueda renderizar el modo correcto desde el primer byte:

  1. Cookie (theme) — leída por layout.tsx server-side, aplicada como className dark en <html>.
  2. localStorage — backup para que el cliente reaplique sin esperar la cookie.
  3. 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.

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.

app/providers.tsx
'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] para currentUserId).
  • API routes / route handlers.

Cuándo NO:

  • Components globales montados en layout.tsx (Header, Footer, Sidebar global).

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.

src/lib/env.ts
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.ts
  • src/lib/jwt.ts
  • src/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.

Reglas de cache en src/lib/api/*:

Tipo de datoEstrategia
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.

Regla: El backend almacena precios en CLP cents (Math.round(precio * 100)).

En el frontend:

  • Para mostrar texto al usuario: usar formatPriceCents(cents) de src/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"

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.

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).