Recientemente, mientras veía a mi hija 🧒🏻 jugar juegos de memoria gratuitos en su tableta, noté que estaba luchando con una abrumadora cantidad de anuncios y molestos banners emergentes.
Esto me inspiró a construir un juego similar para ella. Dado que actualmente le gusta el anime, decidí crear el juego utilizando imágenes lindas de estilo anime.
En este artículo, te guiaré a través del proceso de construcción del juego para ti mismo o tus hijos 🎮.
Comenzaremos explorando las características del juego, luego cubriremos la pila tecnológica y la estructura del proyecto, ambas son directas. Finalmente, discutiremos optimizaciones y aseguraremos un juego fluido en dispositivos móviles 📱.
Si deseas saltar la lectura, aquí 💁 está el repositorio de GitHub 🙌. Y aquí puedes ver la demostración en vivo.
Tabla de Contenidos
Descripción del proyecto
En este tutorial, construiremos un desafiante juego de cartas de memoria con React que prueba tus habilidades de recuerdo. Tu objetivo es hacer clic en imágenes de anime únicas sin hacer clic en la misma dos veces. Cada clic único te otorga puntos, pero ten cuidado, hacer clic en una imagen dos veces reinicia tu progreso.
Características del juego:
-
🎯 Jugabilidad dinámica que desafía tu memoria
-
🔄 Las cartas se mezclan después de cada clic para aumentar la dificultad
-
🏆 Seguimiento de puntaje con persistencia del mejor puntaje
-
😺 Adorables imágenes de anime de The Nekosia API
-
✨ Transiciones y animaciones de carga suaves
-
📱 Diseño responsive para todos los dispositivos
-
🎨 UI limpia y moderna
El juego te ayudará a poner a prueba tus habilidades de memoria mientras disfrutas de lindas imágenes de anime. ¿Puedes lograr la puntuación perfecta?
Cómo Jugar
-
Haz clic en cualquier carta para comenzar
-
Recuerda qué cartas has seleccionado
-
Intenta hacer clic en todas las cartas exactamente una vez
-
Observa cómo crece tu puntuación con cada selección única
-
Luego sigue jugando para intentar superar tu mejor puntuación
El Conjunto Tecnológico
Aquí tienes una lista de las principales tecnologías que estaremos utilizando:
-
NPM – Un gestor de paquetes para JavaScript que ayuda a gestionar dependencias y scripts del proyecto.
-
Vite – Una herramienta de construcción que proporciona un entorno de desarrollo rápido, especialmente optimizado para proyectos web modernos.
-
React – Una popular biblioteca de JavaScript para construir interfaces de usuario, que permite un renderizado eficiente y gestión de estado.
-
Módulos de CSS – Una solución de estilización que delimita el CSS a componentes individuales, evitando conflictos de estilos y asegurando la mantenibilidad.
Construyamos el Juego
Desde este punto en adelante, te guiaré a través del proceso que seguí al construir este juego.
Estructura del Proyecto y Arquitectura
Cuando construí este juego de memoria de cartas, organicé cuidadosamente la base de código para garantizar la mantenibilidad, escalabilidad y una clara separación de preocupaciones. ¡Exploraremos la estructura y el razonamiento detrás de cada decisión:
Arquitectura Basada en Componentes
Seleccioné una arquitectura basada en componentes por varias razones:
-
Modularidad: Cada componente es autocontenido con su propia lógica y estilos
-
Reutilización: Componentes como
Card
yLoader
se pueden reutilizar en toda la aplicación -
Mantenibilidad: Más fácil de depurar y modificar componentes individuales
-
Pruebas: Los componentes se pueden probar de manera aislada
Organización de Componentes
- Componente Card
-
Separado en su propio directorio porque es un elemento central del juego
-
Contiene módulos JSX y SCSS para encapsulación
-
Maneja la renderización de cartas individuales, estados de carga y eventos de clic
- Componente CardsGrid
-
Administra el diseño del tablero de juego
-
Maneja el barajado y distribución de cartas
-
Controla el diseño de cuadrícula receptivo para diferentes tamaños de pantalla
- Componente de carga
-
Indicador de carga reutilizable
-
Mejora la experiencia del usuario durante la carga de imágenes
-
Puede ser utilizado por cualquier componente que necesite estados de carga
- Componentes de Encabezado/Pie de página/Subtítulo
-
Componentes estructurales para el diseño de la aplicación
-
El encabezado muestra el título del juego y las puntuaciones
-
El pie de página muestra la información de derechos de autor y versión
-
El subtítulo proporciona instrucciones del juego
Enfoque de Módulos CSS
Utilicé Módulos CSS (archivos .module.scss
) por varios beneficios:
-
Estilizado con alcance: Evita fugas de estilos entre componentes
-
Colisiones de nombres: Genera automáticamente nombres de clases únicos
-
Mantenibilidad: Los estilos están ubicados junto a sus componentes
-
Funcionalidades de SCSS: Aprovecha las funcionalidades de SCSS manteniendo los estilos modulares
Ganchos personalizados
El directorio hooks
contiene ganchos personalizados como useFetch:
-
Separación de preocupaciones: Aísla la lógica de obtención de datos
-
Reutilización: Puede ser utilizado por cualquier componente que necesite datos de imagen
-
Manejo de estado: Maneja los estados de carga, error y datos
-
Rendimiento: Implementa optimizaciones como el control del tamaño de imagen
Archivos de nivel raíz
App.jsx:
-
Actúa como el punto de entrada de la aplicación
-
Administra el estado global y el enrutamiento (si es necesario)
-
Coordina la composición de componentes
-
Maneja los diseños de nivel superior
Consideraciones de rendimiento
La estructura admite optimizaciones de rendimiento:
-
División de código: Los componentes pueden cargarse de forma perezosa si es necesario
-
Memoización: Los componentes pueden ser memoizados de manera efectiva
-
Carga de estilos: Los Módulos CSS permiten una carga de estilos eficiente
-
Gestión de activos: Las imágenes y los recursos están organizados adecuadamente
Escalabilidad
Esta estructura permite una fácil escalabilidad:
-
Se pueden añadir nuevas funcionalidades como nuevos componentes
-
Se pueden crear ganchos adicionales para nuevas funcionalidades
-
Los estilos siguen siendo mantenibles a medida que la aplicación crece
-
Las pruebas se pueden implementar en cualquier nivel
Experiencia de desarrollo
La estructura mejora la experiencia del desarrollador:
-
Organización clara de archivos
-
Ubicaciones intuitivas de componentes
-
Fácil de encontrar y modificar características específicas
-
Facilita la colaboración eficiente
Esta arquitectura resultó especialmente valiosa al optimizar el juego para su uso en tabletas, ya que me permitió:
-
Identificar y optimizar fácilmente los cuellos de botella de rendimiento
-
Agregar estilos específicos para tabletas sin afectar a otros dispositivos
-
Implementar estados de carga para una mejor experiencia móvil
-
Mantener una separación clara entre la lógica del juego y los componentes de la interfaz de usuario
Bien, ahora vamos a programar.
Guía de construcción paso a paso
1. Configuración del proyecto
Configurar el entorno de desarrollo
Para comenzar con un proyecto React limpio, abre tu aplicación de terminal y ejecuta los siguientes comandos (puedes nombrar la carpeta de tu proyecto como desees, en mi caso el nombre es ‘memory-card’):
npm create vite@latest memory-card -- --template react
cd memory-card
npm install
Instalar las Dependencias Necesarias
Las únicas dependencias que utilizaremos en este proyecto son el paquete de hook de UI.dev (por cierto, aquí puedes encontrar un artículo bien explicado sobre cómo funciona el renderizado en React).
La otra dependencia es el famoso preprocesador CSS, SASS, que necesitaremos para poder escribir nuestros módulos CSS en SASS en lugar de CSS regular.
npm install @uidotdev/usehooks sass
Configurar Vite y Ajustes del Proyecto
Al configurar nuestro proyecto, necesitamos realizar algunos ajustes de configuración específicos para manejar las advertencias de SASS y mejorar nuestra experiencia de desarrollo. Así es como puedes configurar Vite:
// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/setupTests.js'],
css: {
modules: {
classNameStrategy: 'non-scoped'
}
},
preprocessors: {
'**/*.scss': 'sass'
},
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/setupTests.js',
'src/main.jsx',
'src/vite-env.d.ts',
],
},
},
css: {
preprocessorOptions: {
scss: {
quietDeps: true, // Silencia las advertencias de dependencia de SASS
charset: false // Evita la advertencia de charset en las versiones recientes de SASS
}
}
}
});
Ten en cuenta que la mayoría de estas configuraciones se generan automáticamente para ti al crear el proyecto con Vite. Aquí tienes lo que está sucediendo:
-
Configuración de SASS:
-
quietDeps: true
: Esto silencia las advertencias sobre dependencias obsoletas en módulos SASS. Especialmente útil al trabajar con archivos SASS/SCSS de terceros. -
charset: false
: Evita la advertencia “@charset” que aparece en versiones más recientes de SASS al usar caracteres especiales en tus hojas de estilo.
-
-
Configuración de Prueba:
-
globals: true
: Hace que las funciones de prueba estén disponibles globalmente en los archivos de prueba -
environment: 'jsdom'
: Proporciona un entorno DOM para las pruebas -
setupFiles
: Apunta a nuestro archivo de configuración de prueba
-
Estas configuraciones ayudan a crear una experiencia de desarrollo más limpia al eliminar mensajes de advertencia innecesarios en la consola, configurar adecuadamente las configuraciones del entorno de pruebas y garantizar que el procesamiento de SASS/SCSS funcione sin problemas.
Puede ver advertencias en su consola sin estas configuraciones cuando:
-
Usa funciones de SASS/SCSS o importa archivos SASS
-
Ejecuta pruebas que requieren manipulación del DOM
-
Usa caracteres especiales en sus hojas de estilo
2. Construyendo los Componentes
Crear el Componente de Tarjeta
Primero, creemos nuestro componente básico de tarjeta que mostrará imágenes individuales:
// src/components/Card/Card.jsx
import React, { useState, useCallback } from "react";
import Loader from "../Loader";
import styles from "./Card.module.scss";
const Card = React.memo(function Card({ imgUrl, imageId, categoryName, processTurn }) {
const [isLoading, setIsLoading] = useState(true);
const handleImageLoad = useCallback(() => {
setIsLoading(false);
}, []);
const handleClick = useCallback(() => {
processTurn(imageId);
}, [processTurn, imageId]);
return (
<div className={styles.container} onClick={handleClick}>
{isLoading && (
<div className={styles.loaderContainer}>
<Loader message="Loading..." />
</div>
)}
<img
src={imgUrl}
alt={categoryName}
onLoad={handleImageLoad}
className={`${styles.image} ${isLoading ? styles.hidden : ''}`}
/>
</div>
);
});
export default Card;
El componente Card es un bloque de construcción fundamental de nuestro juego. Es responsable de mostrar imágenes individuales y manejar las interacciones del jugador. Veamos su implementación:
Desglose de propiedades:
-
imagen
: (cadena)-
La URL de la imagen que se mostrará y que se recibe de nuestro servicio de API.
-
Se utiliza directamente en el atributo src de la etiqueta img.
-
-
id
: (cadena)-
Identificador único para cada carta que es fundamental para realizar un seguimiento de cuáles cartas se han hecho clic.
-
Se pasa al callback
processTurn
cuando se hace clic en una carta.
-
-
categoría
: (cadena)-
Describe el tipo de imagen (por ejemplo, “anime”, “neko”), y se utiliza en el atributo alt para una mejor accesibilidad.
-
Ayuda con la optimización para motores de búsqueda y lectores de pantalla.
-
-
processTurn
: (función)-
Función de devolución de llamada pasada desde el componente padre que maneja la lógica del juego cuando se hace clic en una carta.
-
También gestiona actualizaciones de puntuación y cambios de estado del juego y determina si una carta ha sido seleccionada anteriormente.
-
-
isLoading
: (boolean)-
Controla si mostrar un estado de carga. Cuando es verdadero, muestra un componente Loader en lugar de la imagen.
-
Mejora la experiencia del usuario durante la carga de la imagen.
-
Estilo del componente:
// src/components/Card/Card.module.scss
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.8);
padding: 20px;
font-size: 30px;
text-align: center;
min-height: 200px;
position: relative;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
.image {
width: 10rem;
height: auto;
opacity: 1;
transition: opacity 0.3s ease;
&.hidden {
opacity: 0;
}
}
.loaderContainer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
Uso en el componente:
<Card
key={getKey()}
imgUrl={item?.image?.original?.url || ""}
imageId={item?.id}
categoryName={item?.category}
processTurn={(imageId) => processTurn(imageId)}
/>
Características clave:
-
Optimización del rendimiento:
-
Utiliza
React.memo
para evitar renderizados innecesarios -
Implementa
useCallback
para manejadores de eventos -
Administra internamente el estado de carga para una mejor UX
-
-
Administración del Estado de Carga:
-
El estado interno
isLoading
sigue la carga de la imagen -
Muestra un componente Loader con un mensaje mientras carga
-
Oculta la imagen hasta que esté completamente cargada usando clases de CSS
-
-
Manejo de Eventos:
-
handleImageLoad
: Administra la transición del estado de carga -
handleClick
: Procesa los movimientos del jugador a través del callbackprocessTurn
-
Construir el Componente CardsGrid
Este es nuestro componente principal del juego que gestiona el estado del juego, la lógica de puntuación y las interacciones con las cartas. Vamos a desglosar su implementación:
// src/components/CardsGrid/CardsGrid.jsx
import React, { useState, useEffect } from "react";
import { useLocalStorage } from "@uidotdev/usehooks";
import Card from "../Card";
import Loader from "../Loader";
import styles from "./CardsGrid.module.scss";
import useFetch from "../../hooks/useFetch";
function CardsGrid(data) {
// Gestión del Estado
const [images, setImages] = useState(data?.data?.images || []);
const [clickedImages, setClickedImages] = useLocalStorage("clickedImages", []);
const [score, setScore] = useLocalStorage("score", 0);
const [bestScore, setBestScore] = useLocalStorage("bestScore", 0);
const [isLoading, setIsLoading] = useState(!data?.data?.images?.length);
// Hook personalizado para obtener imágenes
const { data: fetchedData, fetchData, error } = useFetch();
// Actualizar las imágenes cuando se obtienen nuevos datos
useEffect(() => {
if (fetchedData?.images) {
setImages(fetchedData.images);
setIsLoading(false);
// Restablecer las imágenes seleccionadas cuando se carga un nuevo lote
setClickedImages([]);
}
}, [fetchedData]);
// Función auxiliar para actualizar la mejor puntuación
function updateBestScore(currentScore) {
if (currentScore > bestScore) {
setBestScore(currentScore);
}
}
// Lógica principal del juego
function processTurn(imageId) {
const newClickedImages = [...clickedImages, imageId];
setClickedImages(newClickedImages);
// Si se hace clic en la misma imagen dos veces, restablecer todo
if (clickedImages.includes(imageId)) {
// Actualizar la mejor puntuación si es necesario
updateBestScore(score);
setClickedImages([]);
setScore(0);
} else {
// Manejar la selección exitosa de cartas
const newScore = score + 1;
setScore(newScore);
// Comprobar si se ha obtenido una puntuación perfecta (todas las cartas han sido seleccionadas una vez)
if (newClickedImages.length === images.length) {
updateBestScore(newScore);
fetchData();
setClickedImages([]);
} else {
// Barajar las imágenes
const shuffled = [...images].sort(() => Math.random() - 0.5);
setImages(shuffled);
}
}
}
if (error) {
return <p>Failed to fetch data</p>;
}
if (isLoading) {
return <Loader message="Loading new images..." />;
}
return (
<div className={styles.container}>
{images.map((item) => (
<Card
key={getKey()}
imgUrl={item?.image?.original?.url || ""}
imageId={item?.id}
categoryName={item?.category}
processTurn={(imageId) => processTurn(imageId)}
/>
))}
</div>
);
}
export default React.memo(CardsGrid);
Estilo de Componente:
.container {
display: grid;
gap: 1rem 1rem;
grid-template-columns: auto; /* Por defecto: una columna para dispositivos móviles */
background-color: #2196f3;
padding: 0.7rem;
cursor: pointer;
}
@media (min-width: 481px) {
.container {
grid-template-columns: auto auto; /* Dos columnas para tabletas y superiores */
}
}
@media (min-width: 769px) {
.container {
grid-template-columns: auto auto auto; /* Tres columnas para escritorios y dispositivos más grandes */
}
}
Desglose de las Características Clave:
-
Gestión del Estado:
-
Utiliza
useState
para el estado a nivel de componente -
Implementa
useLocalStorage
para datos de juego persistentes:-
clickedImages
: Registra qué cartas han sido seleccionadas -
score
: Puntuación actual del juego -
bestScore
: Mayor puntuación alcanzada
-
-
Administra el estado de carga para la obtención de imágenes
-
Baraja las cartas
-
-
Lógica del Juego:
-
processTurn
: Maneja los movimientos de los jugadores-
Realiza un seguimiento de los clics duplicados
-
Actualiza las puntuaciones
-
Administra escenarios de puntuación perfecta
-
-
updateBestScore
: Actualiza la mejor puntuación cuando es necesario -
Obtiene automáticamente nuevas imágenes cuando se completa una ronda
-
-
Extracción de Datos:
-
Utiliza el gancho personalizado
useFetch
para los datos de imagen -
Maneja los estados de carga y error
-
Actualiza las imágenes cuando se obtienen nuevos datos
-
-
Optimización del rendimiento:
-
Componente envuelto en
React.memo
-
Actualizaciones de estado eficientes
-
Diseño de cuadrícula receptivo
-
-
Persistencia:
-
El estado del juego persiste en recargas de página
-
Seguimiento de la mejor puntuación
-
Guardado del progreso actual del juego
-
Ejemplo de uso:
...
...
function App() {
const { data, loading, error } = useFetch();
if (loading) return <Loader />;
if (error) return <p>Error: {error}</p>;
return (
<div className={styles.container}>
<Header />
<Subtitle />
<CardsGrid data={data} />
<Footer />
</div>
);
}
export default App;
El componente CardsGrid sirve como el corazón de nuestro juego de memoria de cartas, gestionando:
-
Estado del juego y lógica
-
Seguimiento de puntuación
-
Interacciones de cartas
-
Carga y visualización de imágenes
-
Diseño adaptable
-
Persistencia de datos
Esta implementación proporciona una experiencia de juego fluida manteniendo la legibilidad y mantenibilidad del código a través de una clara separación de responsabilidades y una correcta gestión del estado.
3. Implementar la capa de API
Nuestro juego utiliza una sólida capa de API con múltiples opciones de respaldo para garantizar una entrega fiable de imágenes. Implementemos cada servicio y el mecanismo de respaldo.
Configurar el Servicio API Primario:
// src/services/api/nekosiaApi.js
const NEKOSIA_API_URL = "https://api.nekosia.cat/api/v1/images/catgirl";
export async function fetchNekosiaImages() {
const response = await fetch(
`${NEKOSIA_API_URL}?count=21&additionalTags=white-hair,uniform&blacklistedTags=short-hair,sad,maid&width=300`
);
if (!response.ok) {
throw new Error(`Nekosia API error: ${response.status}`);
}
const result = await response.json();
if (!result.images || !Array.isArray(result.images)) {
throw new Error('Invalid response format from Nekosia API');
}
const validImages = result.images.filter(item => item?.image?.original?.url);
if (validImages.length === 0) {
throw new Error('No valid images received from Nekosia API');
}
return { ...result, images: validImages };
}
Crear el Primer Servicio API de Respaldo:
// src/services/api/nekosBestApi.js
const NEKOS_BEST_API_URL = "https://nekos.best/api/v2/neko?amount=21";
export async function fetchNekosBestImages() {
const response = await fetch(NEKOS_BEST_API_URL, {
method: "GET",
mode: "no-cors"
});
if (!response.ok) {
throw new Error(`Nekos Best API error: ${response.status}`);
}
const result = await response.json();
// Transformar la respuesta para que coincida con nuestro formato esperado
const transformedImages = result.results.map(item => ({
id: item.url.split('/').pop().split('.')[0], // Extraer UUID de la URL
image: {
original: {
url: item.url
}
},
artist: {
name: item.artist_name,
href: item.artist_href
},
source: item.source_url
}));
return { images: transformedImages };
}
Crear el Segundo Servicio API de Respaldo:
// src/services/api/nekosApi.js
const NEKOS_API_URL = "https://api.nekosapi.com/v3/images/random?limit=21&rating=safe";
export async function fetchNekosImages() {
const response = await fetch(NEKOS_API_URL, {
method: "GET",
});
if (!response.ok) {
throw new Error(`Nekos API error: ${response.status}`);
}
const result = await response.json();
// Transformar la respuesta para que coincida con nuestro formato esperado
const transformedImages = result.items.map(item => ({
id: item.id,
image: {
original: {
url: item.image_url
}
}
}));
return { images: transformedImages };
}
Construir el Mecanismo de Respaldo de la API:
// src/services/api/imageService.js
import { fetchNekosiaImages } from "./nekosiaApi";
import { fetchNekosImages } from "./nekosApi";
import { fetchNekosBestImages } from "./nekosBestApi";
export async function fetchImages() {
try {
// Intentar primero la API principal
return await fetchNekosiaImages();
} catch (error) {
console.warn("Primary API failed, trying fallback:", error);
// Probar la primera API de respaldo
try {
return await fetchNekosBestImages();
} catch (fallbackError) {
console.warn("First fallback API failed, trying second fallback:", fallbackError);
// Probar la segunda API de respaldo
try {
return await fetchNekosImages();
} catch (secondFallbackError) {
console.error("All image APIs failed:", secondFallbackError);
throw new Error("All image APIs failed");
}
}
}
}
Usar el Servicio de Imágenes:
// src/hooks/useFetch.js
import { useState, useEffect } from "react";
import { fetchImages } from "../services/api/imageService";
export default function useFetch() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await fetchImages();
setData(result);
} catch (err) {
setError(err.message || 'An error occurred');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return {
data,
loading,
error,
fetchData,
};
}
Características Clave de Nuestra Implementación de API:
-
Varias Fuentes de API:
-
API Principal (Nekosia): Proporciona imágenes de anime de alta calidad
-
Primera Alternativa (Nekos Best): Incluye información del artista
-
Segunda Alternativa (Nekos): Respaldo simple y confiable
-
-
Formato de Datos Consistente:
- Todas las APIs transforman sus respuestas para que coincidan con nuestro formato esperado:
{
images: [
{
id: string,
image: {
original: {
url: string
}
}
}
]
}
-
Manejo de errores robusto:
-
Valida las respuestas de la API
-
Verifica las URL de imagen válidas
-
Proporciona mensajes de error detallados
-
Mecanismo de recuperación elegante
-
-
Funciones de seguridad:
-
Filtrado seguro de contenido (
rating=safe
) -
Limitación de cantidad de imágenes (21 imágenes)
-
Validación de URL
-
Validación del formato de respuesta
-
-
Consideraciones de rendimiento:
-
Tamaños de imagen optimizados
-
Etiquetas de contenido filtradas
-
Transformación eficiente de datos
-
Llamadas mínimas a la API
-
Esta implementación garantiza que nuestro juego tenga una fuente confiable de imágenes mientras maneja posibles fallas de la API con elegancia. El formato de datos consistente en todas las API facilita cambiar entre ellas sin afectar la funcionalidad del juego.
Pruebas de la aplicación
Las pruebas son una parte crucial de cualquier desarrollo de aplicación, y para nuestro Memory Card Game, implementamos una estrategia de pruebas exhaustiva utilizando herramientas y prácticas modernas. Vamos a ver cómo estructuramos nuestras pruebas y algunos patrones clave de pruebas que utilizamos.
Stack de pruebas
-
Vitest: Nuestro marco de pruebas principal, elegido por su rapidez e integración perfecta con Vite
-
React Testing Library: Para probar componentes de React con un enfoque centrado en el usuario
-
@testing-library/user-event: Para simular interacciones de usuario
-
jsdom: Para crear un entorno DOM en nuestras pruebas
Patrones clave de pruebas
Las pruebas fueron una parte crucial para garantizar la fiabilidad y mantenibilidad de este Memory Card Game. Implementé una estrategia de pruebas exhaustiva utilizando React Testing Library y Vitest, centrándome en varias áreas clave:
1. Pruebas de componentes
Escribí pruebas extensas para mis componentes de React para garantizar que se rendericen correctamente y se comporten como se espera. Por ejemplo, el componente CardsGrid
, que es el corazón del juego, tiene una amplia cobertura de pruebas que incluye:
-
Estados de renderización inicial
-
Estados de carga
-
Manejo de errores
-
Seguimiento de puntuación
-
Comportamiento de interacción con las cartas
2. Mocking de pruebas
Para garantizar pruebas fiables y rápidas, implementé varias estrategias de simulación:
-
Operaciones de almacenamiento local utilizando el hook useLocalStorage
-
Llamadas a la API utilizando el hook
useFetch
-
Manejadores de eventos y actualizaciones de estado
3. Mejores prácticas de prueba
En mi implementación de pruebas, seguí varias mejores prácticas:
-
Usar ganchos
beforeEach
yafterEach
para restablecer el estado entre pruebas -
Probar interacciones de usuario usando
fireEvent
de React Testing Library -
Escribir pruebas que se asemejen a cómo interactúan los usuarios con la aplicación
-
Probar tanto escenarios de éxito como de error
-
Aislar pruebas usando un adecuado mocking
4. Herramientas de prueba
El proyecto aprovecha herramientas y bibliotecas modernas de prueba:
-
Vitest: Como ejecutor de pruebas
-
React Testing Library: Para probar componentes de React
-
@testing-library/jest-dom: Para afirmaciones de pruebas de DOM mejoradas
-
@testing-library/user-event: Para simular interacciones de usuario
Este enfoque integral de pruebas me ayudó a detectar errores temprano, garantizó la calidad del código, y facilitó y hizo más seguro el proceso de refactorización.
Optimizaciones
Para garantizar un rendimiento fluido, especialmente en dispositivos móviles, implementamos varias técnicas de optimización:
-
Transformación de respuestas
-
Formato de datos estandarizado en todas las API
-
Extracción eficiente de ID desde URLs
-
Metadatos de imagen estructurados para acceso rápido
-
-
Optimización de red
-
Uso del modo
no-cors
cuando sea adecuado para manejar eficientemente problemas de CORS -
Manejo de errores con códigos de estado específicos para una mejor depuración
-
Estructura de respuesta consistente en todas las implementaciones de API
-
-
Consideraciones Mobile-First
-
Estrategia optimizada de carga de imágenes
-
Manejo eficiente de errores para prevenir reintentos innecesarios
-
Transformación de datos simplificada para reducir la sobrecarga de procesamiento
-
Mejoras Futuras
Hay algunas formas en que podríamos mejorar aún más este proyecto:
-
Cacheo de Respuestas de API
-
Implementar cacheo en almacenamiento local para imágenes de uso frecuente
-
Agregar estrategia de invalidación de caché para contenido fresco
-
Implementar carga progresiva de imágenes
-
-
Optimización del Rendimiento
-
Agregar carga diferida de imágenes para mejorar el tiempo de carga inicial
-
Implementar colas de solicitudes para una mejor gestión del ancho de banda
-
Agregar compresión de respuestas para una transferencia de datos más rápida
-
-
Mejoras en la Confiabilidad
-
Agregar verificación de salud de la API antes de los intentos
-
Implementar mecanismos de reintento con retroceso exponencial
-
Agregar patrón de cortacircuitos para APIs que fallan
-
-
Analíticas y Monitoreo
-
Rastrear tasas de éxito de API
-
Monitorear tiempos de respuesta
-
Implementar cambio automático de API basado en métricas de rendimiento
-
Esta implementación robusta asegura que nuestro juego permanezca funcional y con buen rendimiento incluso bajo condiciones adversas de red o indisponibilidad de API, mientras mantiene espacio para futuras mejoras y optimizaciones.
Conclusión
Construir este Juego de Cartas de Memoria ha sido más que solo crear una alternativa divertida y sin anuncios para los niños; ha sido un ejercicio en la implementación de las mejores prácticas modernas de desarrollo web mientras se resuelve un problema del mundo real.
El proyecto demuestra cómo combinar una arquitectura reflexiva, pruebas robustas y mecanismos de respaldo confiables puede resultar en una aplicación lista para producción que es tanto entretenida como educativa.
🗝️ Conclusiones Clave
-
Desarrollo Centrado en el Usuario
-
Comenzó con un problema claro (juegos llenos de anuncios que afectan la experiencia del usuario)
-
Implementó funciones que mejoran la jugabilidad sin interrupciones
-
Mantuvo el enfoque en el rendimiento y la confiabilidad en todos los dispositivos
-
-
Excelencia Técnica
-
Utilizó patrones y hooks modernos de React para un código limpio y mantenible
-
Implementó una estrategia de pruebas exhaustiva que garantiza la confiabilidad
-
Creó un sistema robusto de respaldo de API para una jugabilidad ininterrumpida
-
-
Rendimiento Primero
-
Adoptó un enfoque móvil primero con diseño receptivo
-
Optimización de carga y manejo de imágenes
-
Implementó estrategias eficientes de gestión de estado y almacenamiento en caché
-
📚 Resultados del Aprendizaje
Este proyecto muestra cómo juegos aparentemente simples pueden ser excelentes vehículos para implementar soluciones técnicas complejas. Desde la arquitectura de componentes hasta los respaldos de API, cada característica se construyó teniendo en cuenta la escalabilidad y mantenibilidad, demostrando que incluso los proyectos de hobby pueden mantener una calidad de código de nivel profesional.
🔮 Avanzando
Aunque el juego logra con éxito su objetivo principal de proporcionar una experiencia agradable sin anuncios, las mejoras futuras documentadas ofrecen un claro camino a seguir para la evolución. Ya sea implementando optimizaciones adicionales o agregando nuevas características, la base es sólida y está lista para expandirse.
El Juego de Memoria es un testimonio de cómo los proyectos personales pueden resolver problemas del mundo real y servir como plataformas para implementar las mejores prácticas en el desarrollo web moderno. ¡Siéntete libre de explorar el código, contribuir o usarlo como inspiración para tus propios proyectos!
Source:
https://www.freecodecamp.org/news/how-to-build-a-memory-card-game-using-react/