Hace años, las Pokédex web eran simples: carga un JSON, renderiza un grid, espera que el navegador no muera. Hoy esperas lo contrario: que una web con 1025 Pokémon te devuelva resultados antes de que termines de escribir.
PokeCore nació del desafío opuesto: no es "¿cómo muestro todos?" sino "¿cómo hago que parezca que no hay tantos?"
Caché en cascada: reducir network a casi cero
Hay tres niveles de caché en PokeCore, y es deliberado.
Primero: localStorage. Cuando abres PokeCore, tu navegador ya sabe qué Pokémon marcaste como favorito la última vez. Cero red. Luego: TanStack Query. Mientras navegas entre regiones, Query caché los datos que ya pediste. No los pide dos veces. Finalmente: HTTP. La API PokéAPI es lenta desde Latinoamérica — pero Solo se consulta una región a la vez.
La pregunta clave fue: ¿cuándo prefetch el resto? La respuesta es el segundo que abres la app. Mientras ves la pantalla de inicio (selector de región), en background Query ya está pidiendo los datos de Kanto. Para cuando hagas clic, ya está todo aquí. No esperas nada.
Esto requiere que entiendas el flujo del usuario antes de codificar. No es "prefetch todo". Es "prefetch lo que el usuario probablemente querrá ver".
Virtualization: renderizar lo que ves, no lo que existe
1025 Pokémon son demasiados para renderizar. Pero también son innecesarios: si el grid tiene 4 columnas y caben 8 filas en pantalla, solo necesito 32 elementos renderizados. Los otros 993 pueden esperar.
La técnica se llama virtualization. React Query + Framer Motion hacen el resto: conforme scrolleas, las tarjetas que salen de pantalla se destroyen, las nuevas se renderan. Es invisible. Las animaciones siguen siendo suaves porque Framer Motion no bloquea el thread principal.
El resultado: incluso en móvil, 1025 Pokémon es tan rápido como 50.
Tipos dinámicos: 18 colores, un solo sistema
Cada Pokémon tiene un tipo o dos. Agua, Fuego, Tierra, Psíquico... son 18 tipos totales, cada uno con un color específico.
La solución fácil: hardcodear 18 valores de color en CSS. La solución correcta: pasarlos como tema a Tailwind en compilación. Eso me permite hacer dos cosas. Primero: los colores son fuente única — si mañana me pido un verde menos agresivo, cambio un número y todos los Pokémon de tipo Planta se actualizan. Segundo: los colores aparecen en pills, backgrounds, glows — todo coherente sin duplicación.
No es un detalle cosmético. Es arquitectura de diseño. Sistema de colores que responde a datos.
Responsive sin "breakpoints": escala fluida
El grid tiene 4 columnas en desktop, 3 en tablet, 2 en móvil. Algunos cambios necesitan un breakpoint. Pero la mayoría debería ser fluida: si tienes 375px de ancho, esperas 2 columnas. Si tienes 390px, también. El paso de 3→2 debería ser invisible.
Tailwind tiene grid-cols-auto-fit — un feature CSS nativo que calcula el número de columnas automáticamente basado en el ancho disponible y un mínimo de columna. Sin breakpoints. Sin decisiones arbitrarias. Solo CSS.
Modal glass con fallback en cascada
Cuando haces clic en un Pokémon, el modal que aparece tiene una imagen. La imagen viene de PokéAPI con tres fuentes posibles: official-artwork (mejor), home (aceptable), front_default (básica).
Si official-artwork no existe, carga home. Si home tampoco, carga front_default. Si nada existe, renderizan un placeholder.
Esta lógica vive en un componente SpriteImg reutilizable. En lugar de que cada pantalla maneje sus propios fallbacks, la responsabilidad es clara: "este componente renderiza una imagen de Pokémon, con fallbacks". Quien lo use no toca eso.
Favoritos: síncrono en cliente, persistente sin servidor
Los Pokémon que marques como favorito se guardan en localStorage. No toca servidor. Instantáneo. Pero no efímero — cierra la pestaña, abre mañana, tus favoritos están ahí.
Zustand maneja el estado global. Cuando marques un favorito, Zustand actualiza el estado en memoria y localStorage. El sidebar se renderiza desde Zustand. Cambio en un click.
La complejidad que evité: sincronización con servidor, login, autenticación. Los favoritos no necesitaban eso. localStorage es suficiente. La arquitectura dice "no hagas lo que no necesitas".
Lo que aprendí construyendo PokeCore
La pregunta inicial era "¿puedo hacer una Pokédex web completa?" La respuesta fue sí, pero no por agregar features. Por entender dónde era la fricción: network lento, renderizado de demasiados elementos, decisiones de diseño sin sistema.
Cada decisión resolvia un problema real. Prefetch porque la red es lenta. Virtualization porque el navegador no puede renderizar 1025 elementos. Caché en cascada porque cada milisegundo importa en móvil.
No es una app compleja. Es un app simple hecha con criterio en cada capa.
