Recentemente, enquanto assistia minha filha 🧒🏻 jogar jogos de memória gratuitos em seu tablet, notei que ela estava tendo dificuldades com uma quantidade avassaladora de anúncios e banners pop-up irritantes.
Isso me inspirou a construir um jogo semelhante para ela. Como ela está atualmente interessada em animes, decidi criar o jogo usando imagens fofas no estilo anime.
Neste artigo, vou orientá-lo pelo processo de construção do jogo para você ou seus filhos 🎮.
Começaremos explorando as características do jogo, e depois abordaremos a pilha de tecnologia e a estrutura do projeto — ambas são simples. Por fim, discutiremos otimizações e garantiremos uma jogabilidade suave em dispositivos móveis 📱.
Se você quiser pular a leitura, aqui 💁 está o repositório do GitHub 🙌. E aqui você pode ver a demonstração ao vivo.
Índice
Descrição do Projeto
Neste tutorial, vamos construir um desafiador jogo de memória de cartas com React que testa suas habilidades de recordação. Seu objetivo é clicar em imagens de anime únicas sem clicar na mesma duas vezes. Cada clique único ganha pontos, mas cuidado – clicar em uma imagem duas vezes redefine seu progresso.
Recursos do jogo:
-
🎯 Jogabilidade dinâmica que desafia sua memória
-
🔄 As cartas são embaralhadas após cada clique para aumentar a dificuldade
-
🏆 Rastreamento de pontuação com persistência da melhor pontuação
-
😺 Imagens de anime adoráveis da API The Nekosia
-
✨ Transições e animações de carregamento suaves
-
📱 Design responsivo para todos os dispositivos
-
🎨 UI limpa e moderna
O jogo ajudará você a testar suas habilidades de memória enquanto desfruta de lindas imagens de anime. Você consegue alcançar a pontuação perfeita?
Como Jogar
-
Clique em qualquer carta para começar
-
Lembre-se de quais cartas você clicou
-
Tente clicar em todas as cartas exatamente uma vez
-
Veja sua pontuação crescer a cada seleção única
-
Depois continue jogando para tentar superar sua melhor pontuação
A Pilha de Tecnologias
Aqui está uma lista das principais tecnologias que usaremos:
-
NPM – Um gerenciador de pacotes para JavaScript que ajuda a gerenciar dependências e scripts para o projeto.
-
Vite – Uma ferramenta de construção que fornece um ambiente de desenvolvimento rápido, especialmente otimizado para projetos web modernos.
-
React – Uma biblioteca JavaScript popular para a construção de interfaces de usuário, permitindo renderização eficiente e gerenciamento de estado.
-
Módulos CSS – Uma solução de estilização que delimita o CSS para componentes individuais, evitando conflitos de estilo e garantindo a manutenibilidade.
Vamos Construir o Jogo
De agora em diante, vou orientá-lo pelo processo que segui ao construir este jogo.
Arquitetura e Estrutura do Projeto
Ao construir este jogo de cartas de memória, organizei cuidadosamente a base de código para garantir manutenibilidade, escalabilidade e clara separação de preocupações. Vamos explorar a estrutura e o raciocínio por trás de cada decisão:
Arquitetura Baseada em Componentes
Optei por uma arquitetura baseada em componentes por várias razões:
-
Modularidade: Cada componente é autônomo, com sua própria lógica e estilos
-
Reutilização: Componentes como
Card
eLoader
podem ser reutilizados em toda a aplicação -
Manutenabilidade: Mais fácil de depurar e modificar componentes individuais
-
Teste: Componentes podem ser testados em isolamento
Organização de Componentes
- Componente Card
-
Separado em seu próprio diretório porque é um elemento central do jogo
-
Contém módulos JSX e SCSS para encapsulamento
-
Gerencia a renderização individual de cartas, estados de carregamento e eventos de clique
- Componente CardsGrid
-
Gerencia o layout do tabuleiro do jogo
-
Gerencia a mistura e distribuição das cartas
-
Controla o layout de grade responsivo para diferentes tamanhos de tela
- Componente de Carregamento
-
Indicador de carregamento reutilizável
-
Melhora a experiência do usuário durante o carregamento de imagens
-
Pode ser usado por qualquer componente que necessita de estados de carregamento
- Componentes de Cabeçalho/Rodapé/Subtítulo
-
Componentes estruturais para o layout do aplicativo
-
O cabeçalho exibe o título do jogo e pontuações
-
O rodapé mostra informações de direitos autorais e versão
-
O subtítulo fornece instruções do jogo
Abordagem de Módulos CSS
Utilizei Módulos CSS (arquivos .module.scss
) por diversos benefícios:
-
Estilização Limitada: Evita vazamentos de estilos entre componentes
-
Colisões de Nomes: Gera automaticamente nomes de classes únicos
-
Manutenibilidade: Estilos estão co-localizados com seus componentes
-
Recursos SCSS: Aproveita recursos SCSS enquanto mantém os estilos modulares
Hooks Personalizados
O diretório hooks
contém hooks personalizados como useFetch:
-
Separação de Preocupações: Isola a lógica de busca de dados
-
Reutilizabilidade: Pode ser usado por qualquer componente que necessite de dados de imagem
-
Gerenciamento de Estado: Lida com estados de carregamento, erro e dados
-
Desempenho: Implementa otimizações como controle de tamanho de imagem
Arquivos de Nível Raiz
App.jsx:
-
Atua como o ponto de entrada da aplicação
-
Gerencia o estado global e o roteamento (se necessário)
-
Coordena a composição dos componentes
-
Trata dos layouts de alto nível
Considerações de Desempenho
A estrutura suporta otimizações de desempenho:
-
Divisão de Código: Os componentes podem ser carregados sob demanda, se necessário
-
Memoização: Os componentes podem ser memoizados de forma eficaz
-
Carregamento de Estilos: Os Módulos CSS possibilitam um carregamento eficiente de estilos
-
Gerenciamento de Ativos: Imagens e recursos são organizados adequadamente
Escalabilidade
Esta estrutura permite uma escalabilidade fácil:
-
Novos recursos podem ser adicionados como novos componentes
-
Novos hooks podem ser criados para novas funcionalidades
-
Os estilos permanecem mantíveis conforme o aplicativo cresce
-
Os testes podem ser implementados em qualquer nível
Experiência de Desenvolvimento
A estrutura aprimora a experiência do desenvolvedor:
-
Organização clara dos arquivos
-
Locais de componentes intuitivos
-
Fácil encontrar e modificar funcionalidades específicas
-
Suporta colaboração eficiente
Esta arquitetura se mostrou particularmente valiosa ao otimizar o jogo para uso em tablets, pois me permitiu:
-
Identificar e otimizar facilmente gargalos de desempenho
-
Adicionar estilos específicos para tablet sem afetar outros dispositivos
-
Implementar estados de carregamento para uma melhor experiência móvel
-
Manter uma separação clara entre a lógica do jogo e os componentes da interface do usuário
Ok, agora vamos começar a codificar.
Guia de Construção Passo a Passo
1. Configuração do Projeto
Configurar o Ambiente de Desenvolvimento
Para começar um projeto React limpo, abra o seu aplicativo de terminal e execute os seguintes comandos (você pode nomear a pasta do seu projeto como desejar – no meu caso, o nome é ‘memory-card’):
npm create vite@latest memory-card -- --template react
cd memory-card
npm install
Instalar as Dependências Necessárias
As únicas dependências que iremos utilizar neste projeto são o pacote hook da UI.dev (aliás, aqui você pode encontrar um artigo bem explicado sobre como o rendering no React funciona).
A outra dependência é o famoso pré-processador CSS, SASS, que precisaremos para escrever nossos módulos CSS em SASS em vez de CSS regular.
npm install @uidotdev/usehooks sass
Configurar o Vite e as Configurações do Projeto
Ao configurar nosso projeto, precisamos fazer alguns ajustes específicos de configuração para lidar com avisos do SASS e melhorar nossa experiência de desenvolvimento. Veja como você pode configurar o Vitest:
// 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 os avisos de dependência do SASS
charset: false // Impede o aviso de charset em versões recentes do SASS
}
}
}
});
Tenha em mente que a maioria dessas configurações são geradas automaticamente para você ao criar o projeto com o Vite. Aqui está o que está acontecendo:
-
Configuração SASS:
-
quietDeps: true
: Isso silencia os avisos sobre dependências obsoletas em módulos SASS. Particularmente útil ao trabalhar com arquivos SASS/SCSS de terceiros. -
charset: false
: Impede o aviso “@charset” que aparece nas versões mais recentes do SASS ao usar caracteres especiais em suas folhas de estilo.
-
-
Configuração de Teste:
-
globals: true
: Torna as funções de teste globalmente disponíveis em arquivos de teste -
environment: 'jsdom'
: Fornece um ambiente DOM para testes -
setupFiles
: Aponta para nosso arquivo de configuração de teste
-
Essas configurações ajudam a criar uma experiência de desenvolvimento mais limpa, removendo mensagens de aviso desnecessárias no console, configurando adequadamente o ambiente de teste e garantindo que o processamento SASS/SCSS funcione sem problemas.
Você pode ver avisos no seu console sem essas configurações quando:
-
Usar recursos SASS/SCSS ou importar arquivos SASS
-
Executar testes que requerem manipulação do DOM
-
Usar caracteres especiais em suas folhas de estilo
2. Construindo os Componentes
Criar o Componente Card
Primeiro, vamos criar nosso componente de cartão básico que exibirá imagens individuais:
// 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;
O componente Card é um bloco de construção fundamental do nosso jogo. Ele é responsável por exibir imagens individuais e lidar com interações dos jogadores. Vamos detalhar sua implementação:
Quebra de propriedades:
-
imagem
: (string)-
A URL da imagem a ser exibida que é recebida do nosso serviço de API.
-
Ela é usada diretamente no atributo src da tag img.
-
-
id
: (string)-
Identificador único para cada carta que é crítico para rastrear quais cartas foram clicadas.
Ele é passado para o callback
processTurn
quando uma carta é clicada. -
-
categoria
: (string)-
Descreve o tipo de imagem (por exemplo, “anime”, “neko”), e é usado no atributo alt para melhor acessibilidade.
-
Ajuda com SEO e leitores de tela.
-
-
processTurn
: (função)-
Função de retorno passada do componente pai que lida com a lógica do jogo quando uma carta é clicada.
-
Também gerencia atualizações de pontuação e mudanças de estado do jogo e determina se uma carta já foi clicada antes.
-
-
isLoading
: (booleano)-
Controla se mostrar um estado de carregamento. Quando verdadeiro, exibe um componente Loader em vez da imagem.
Melhora a experiência do usuário durante o carregamento da imagem.
-
Estilização do 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 no componente:
<Card
key={getKey()}
imgUrl={item?.image?.original?.url || ""}
imageId={item?.id}
categoryName={item?.category}
processTurn={(imageId) => processTurn(imageId)}
/>
Principais características:
-
Otimização de Desempenho:
-
Utiliza
React.memo
para evitar re-renderizações desnecessárias -
Implementa
useCallback
para manipuladores de eventos -
Gerencia o estado de carregamento internamente para melhor UX
-
-
Gerenciamento de Estado de Carregamento:
-
Estado interno
isLoading
rastreia o carregamento da imagem -
Exibe um componente Loader com uma mensagem enquanto carrega
-
Oculta a imagem até que esteja totalmente carregada usando classes CSS
-
-
Manipulação de Eventos:
-
handleImageLoad
: Gerencia a transição do estado de carregamento -
handleClick
: Processa os movimentos do jogador através do callbackprocessTurn
-
Construa o Componente CardsGrid
Este é o nosso componente principal do jogo que gerencia o estado do jogo, a lógica de pontuação e as interações com as cartas. Vamos detalhar sua implementação:
// 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) {
// Gerenciamento de 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 buscar imagens
const { data: fetchedData, fetchData, error } = useFetch();
// Atualizar imagens quando novos dados são buscados
useEffect(() => {
if (fetchedData?.images) {
setImages(fetchedData.images);
setIsLoading(false);
// Redefinir imagens clicadas quando um novo lote é carregado
setClickedImages([]);
}
}, [fetchedData]);
// Função auxiliar para atualizar a melhor pontuação
function updateBestScore(currentScore) {
if (currentScore > bestScore) {
setBestScore(currentScore);
}
}
// Lógica principal do jogo
function processTurn(imageId) {
const newClickedImages = [...clickedImages, imageId];
setClickedImages(newClickedImages);
// Se clicar na mesma imagem duas vezes, redefinir tudo
if (clickedImages.includes(imageId)) {
// Atualizar a melhor pontuação, se necessário
updateBestScore(score);
setClickedImages([]);
setScore(0);
} else {
// Lidar com a seleção bem-sucedida de cartas
const newScore = score + 1;
setScore(newScore);
// Verificar se a pontuação perfeita foi alcançada (todas as cartas clicadas uma vez)
if (newClickedImages.length === images.length) {
updateBestScore(newScore);
fetchData();
setClickedImages([]);
} else {
// Embaralhar as imagens
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);
Estilização do Componente:
.container {
display: grid;
gap: 1rem 1rem;
grid-template-columns: auto; /* Padrão: uma coluna para dispositivos móveis */
background-color: #2196f3;
padding: 0.7rem;
cursor: pointer;
}
@media (min-width: 481px) {
.container {
grid-template-columns: auto auto; /* Duas colunas para tablets e acima */
}
}
@media (min-width: 769px) {
.container {
grid-template-columns: auto auto auto; /* Três colunas para desktops e maiores */
}
}
Detalhes das Principais Funcionalidades:
-
Gerenciamento de Estado:
-
Usa
useState
para estado a nível de componente -
Implementa
useLocalStorage
para dados persistentes do jogo:-
clickedImages
: Rastreia quais cartas foram clicadas -
score
: Pontuação atual do jogo -
bestScore
: Maior pontuação alcançada
-
-
Gerencia o estado de carregamento para busca de imagens
-
Embaralha as cartas
-
-
Lógica do Jogo:
-
processTurn
: Lida com as jogadas dos jogadores-
Rastreia cliques duplicados
-
Atualiza pontuações
-
Gerencia cenários de pontuação perfeita
-
-
updateBestScore
: Atualiza a pontuação máxima quando necessário -
Busca automaticamente novas imagens quando uma rodada é concluída
-
-
Busca de Dados:
-
Usa o hook customizado
useFetch
para dados de imagem -
Gerencia estados de carregamento e erro
-
Atualiza imagens quando novos dados são buscados
-
-
Otimização de Performance:
-
Componente envolto em
React.memo
-
Atualizações de estado eficientes
-
Layout de grade responsivo
-
-
Persistência:
-
O estado do jogo persiste entre recarregamentos de página
-
Salvamento do progresso atual do jogo
Rastreamento da melhor pontuação
-
Exemplo 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;
O componente CardsGrid serve como o coração do nosso jogo de cartas de memória, gerenciando:
-
Estado e lógica do jogo
-
Rastreamento de pontuação
-
Interações de cartas
-
Carregamento e exibição de imagens
-
Layout responsivo
-
Persistência de dados
Esta implementação fornece uma experiência de jogo suave, mantendo a legibilidade e a manutenibilidade do código por meio de uma clara separação de preocupações e um gerenciamento adequado do estado.
3. Implementando a Camada de API
Nosso jogo utiliza uma camada de API robusta com várias opções de fallback para garantir a entrega confiável de imagens. Vamos implementar cada serviço e o mecanismo de fallback.
Configurar o Serviço de API Primário:
// 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 };
}
Criar o Primeiro Serviço de API de Fallback:
// 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 a resposta para corresponder ao nosso formato esperado
const transformedImages = result.results.map(item => ({
id: item.url.split('/').pop().split('.')[0], // Extrair UUID do URL
image: {
original: {
url: item.url
}
},
artist: {
name: item.artist_name,
href: item.artist_href
},
source: item.source_url
}));
return { images: transformedImages };
}
Criar o Segundo Serviço de API de Fallback:
// 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 a resposta para corresponder ao nosso formato esperado
const transformedImages = result.items.map(item => ({
id: item.id,
image: {
original: {
url: item.image_url
}
}
}));
return { images: transformedImages };
}
Construa o Mecanismo de Reserva da API:
// src/services/api/imageService.js
import { fetchNekosiaImages } from "./nekosiaApi";
import { fetchNekosImages } from "./nekosApi";
import { fetchNekosBestImages } from "./nekosBestApi";
export async function fetchImages() {
try {
// Tente primeiro a API principal
return await fetchNekosiaImages();
} catch (error) {
console.warn("Primary API failed, trying fallback:", error);
// Tente a primeira API de reserva
try {
return await fetchNekosBestImages();
} catch (fallbackError) {
console.warn("First fallback API failed, trying second fallback:", fallbackError);
// Tente a segunda API de reserva
try {
return await fetchNekosImages();
} catch (secondFallbackError) {
console.error("All image APIs failed:", secondFallbackError);
throw new Error("All image APIs failed");
}
}
}
}
Use o Serviço de Imagens:
// 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,
};
}
Principais Recursos de Nossa Implementação de API:
-
Múltiplas Fontes de API:
-
API Principal (Nekosia): Fornece imagens de anime de alta qualidade
-
Primeira Reserva (Nekos Best): Inclui informações do artista
-
Segunda Reserva (Nekos): Backup simples e confiável
-
-
Formato de Dados Consistente:
- Todas as APIs transformam suas respostas para corresponder ao nosso formato esperado:
{
images: [
{
id: string,
image: {
original: {
url: string
}
}
}
]
}
-
Manuseio de Erros Robusto:
-
Valida as respostas da API
-
Verifica URLs de imagem válidas
-
Fornece mensagens de erro detalhadas
-
Mecanismo de fallback gracioso
-
-
Recursos de Segurança:
-
Filtragem segura de conteúdo (
classificação=seguro
) -
Limitação de contagem de imagens (21 imagens)
-
Validação de URL
-
Validação do formato de resposta
-
-
Considerações de Desempenho:
-
Tamanhos de imagem otimizados
-
Tags de conteúdo filtradas
-
Transformação eficiente de dados
-
Chamadas mínimas à API
-
Esta implementação garante que nosso jogo tenha uma fonte confiável de imagens, lidando de forma adequada com possíveis falhas na API. O formato de dados consistente em todas as APIs facilita a troca entre elas sem afetar a funcionalidade do jogo.
Testando o Aplicativo
Testar é uma parte crucial do desenvolvimento de qualquer aplicativo, e para o nosso Memory Card Game, implementamos uma estratégia abrangente de testes usando ferramentas e práticas modernas. Vamos mergulhar na estrutura dos nossos testes e em alguns padrões-chave de teste que utilizamos.
Pilha de Testes
-
Vitest: Nosso framework de testes principal, escolhido pela sua rapidez e integração perfeita com o Vite
-
React Testing Library: Para testar componentes React com uma abordagem centrada no usuário
-
@testing-library/user-event: Para simular interações do usuário
-
jsdom: Para criar um ambiente DOM em nossos testes
Principais Padrões de Teste
Os testes foram uma parte crucial para garantir a confiabilidade e a manutenibilidade deste Jogo de Memória. Eu implementei uma estratégia abrangente de testes usando a React Testing Library e o Vitest, focando em várias áreas-chave:
1. Teste de Componentes
Escrevi testes extensivos para meus componentes React para garantir que eles renderizassem corretamente e se comportassem como esperado. Por exemplo, o componente CardsGrid
, que é o coração do jogo, possui uma cobertura de testes completa, incluindo:
-
Estados de renderização inicial
-
Estados de carregamento
-
Tratamento de erros
-
Rastreamento de pontuação
-
Comportamento de interação das cartas
2. Mocking de Testes
Para garantir testes confiáveis e rápidos, implementei várias estratégias de mocking:
-
Operações de armazenamento local usando o hook useLocalStorage
-
Chamadas de API usando o hook
useFetch
-
Manipuladores de eventos e atualizações de estado
3. Melhores Práticas de Testes
Ao longo da minha implementação de testes, segui várias melhores práticas:
-
Usando
beforeEach
eafterEach
hooks para redefinir o estado entre os testes -
Testando interações do usuário usando
fireEvent
da React Testing Library -
Escrevendo testes que se assemelham a como os usuários interagem com o aplicativo
-
Testando tanto cenários de sucesso quanto de erro
-
Isolando testes usando simulação adequada
4. Ferramentas de Teste
O projeto aproveita ferramentas e bibliotecas modernas de teste:
-
Vitest: Como o executor de testes
-
React Testing Library: Para testar componentes React
-
@testing-library/jest-dom: Para afirmações de teste de DOM aprimoradas
-
@testing-library/user-event: Para simular interações do usuário
Essa abordagem abrangente de teste me ajudou a identificar bugs precocemente, garantiu a qualidade do código e tornou o refatoramento mais seguro e gerenciável.
Otimizações
Para garantir um desempenho suave, especialmente em dispositivos móveis, implementamos várias técnicas de otimização:
-
Transformação de Resposta
-
Formato de dados padronizado em todas as APIs
-
Extração eficiente de IDs a partir de URLs
-
Metadados de imagem estruturados para acesso rápido
-
-
Otimização de Rede
-
Usando o modo
no-cors
quando apropriado para lidar com problemas de CORS de forma eficiente -
Tratamento de erros com códigos de status específicos para melhor depuração
-
Estrutura de resposta consistente em todas as implementações de API
-
-
Considerações Mobile-First
-
Estratégia otimizada de carregamento de imagens
-
Tratamento de erros eficiente para evitar tentativas desnecessárias
-
Transformação de dados simplificada para reduzir a sobrecarga de processamento
-
Melhorias Futuras
Existem algumas maneiras de melhorar ainda mais este projeto:
-
Cache de Respostas da API
-
Implementar cache de armazenamento local para imagens frequentemente usadas
-
Adicionar estratégia de invalidação de cache para conteúdo atualizado
-
Implementar carregamento progressivo de imagens
-
-
Otimizações de Performance
-
Adicione carregamento preguiçoso de imagens para melhor tempo de carregamento inicial
-
Implemente o enfileiramento de requisições para melhor gerenciamento de largura de banda
-
Adicione compressão de resposta para transferência de dados mais rápida
-
-
Aprimoramentos de Confiabilidade
-
Adicione verificação de saúde da API antes das tentativas
-
Implemente mecanismos de nova tentativa com aumento exponencial
-
Adicione o padrão de disjuntor para APIs que falham
-
-
Analytics e Monitoramento
-
Acompanhe as taxas de sucesso da API
-
Monitore os tempos de resposta
-
Implemente a troca automática de API com base em métricas de desempenho
-
Essa implementação robusta garante que nosso jogo permaneça funcional e performático mesmo sob condições adversas de rede ou indisponibilidade da API, ao mesmo tempo que ainda mantém espaço para futuras melhorias e otimizações.
Conclusão
Construir este Jogo de Memória foi mais do que apenas criar uma alternativa divertida e sem anúncios para crianças—foi um exercício na implementação das melhores práticas de desenvolvimento web moderno enquanto resolvia um problema do mundo real.
O projeto demonstra como a combinação de uma arquitetura bem pensada, testes robustos e mecanismos de fallback confiáveis pode resultar em uma aplicação pronta para produção que é tanto entretenimento quanto educacional.
🗝️ Principais Conclusões
-
Desenvolvimento Centrado no Usuário
-
Começou com um problema claro (jogos repletos de anúncios afetando a experiência do usuário)
-
Implementou recursos que melhoram a jogabilidade sem interrupções
-
Manteve o foco em desempenho e confiabilidade em todos os dispositivos
-
-
Excelência Técnica
-
Utilizou padrões e hooks modernos do React para código limpo e de fácil manutenção
-
Implementou uma estratégia de testes abrangente garantindo confiabilidade
-
Criou um sistema robusto de fallback da API para jogabilidade ininterrupta
-
-
Desempenho em Primeiro Lugar
-
Adotada a abordagem mobile-first com design responsivo
-
Otimizada a carga e o manuseio de imagens
-
Implementadas estratégias eficientes de gerenciamento de estado e caching
-
📚 Resultados de Aprendizado
Este projeto demonstra como jogos aparentemente simples podem ser excelentes veículos para implementar soluções técnicas complexas. Desde a arquitetura de componentes até os fallbacks de API, cada recurso foi construído com escalabilidade e manutenibilidade em mente, provando que até projetos de hobby podem manter uma qualidade de código profissional.
🔮 Avançando
Embora o jogo atinja com sucesso seu objetivo principal de fornecer uma experiência agradável e sem anúncios, as melhorias futuras documentadas fornecem um roteiro claro para evolução. Seja implementando otimizações adicionais ou adicionando novos recursos, a fundação é sólida e pronta para expansão.
O Jogo da Memória é um testemunho de como projetos pessoais podem tanto resolver problemas do mundo real quanto servir como plataformas para implementar as melhores práticas no desenvolvimento web moderno. Sinta-se à vontade para explorar o código, contribuir ou usá-lo como inspiração para seus próprios projetos!
Source:
https://www.freecodecamp.org/news/how-to-build-a-memory-card-game-using-react/