Recentemente, enquanto assistia minha filha 🧒🏻 jogando 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 te guiar pelo processo de construção do jogo para você ou seus filhos 🎮.
Vamos começar explorando as características do jogo, depois abordaremos a pilha tecnológica e a estrutura do projeto – ambas são diretas. 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 cartas de memória 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
-
🔄 Cartas embaralham 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 suaves de carregamento
-
📱 Design responsivo para todos os dispositivos
-
🎨 UI limpa e moderna
O jogo ajudará você a testar suas habilidades de memória enquanto desfruta de imagens de anime fofas. 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 com 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 estaremos usando:
-
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 construir interfaces de usuário, permitindo renderização eficiente e gerenciamento de estado.
-
CSS Modules – 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
A partir deste ponto, eu irei guiá-lo através do processo que segui ao construir este jogo.
Estrutura do Projeto e Arquitetura
Ao construir este jogo de cartas de memória, eu organizei cuidadosamente a base de código para garantir a 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
Eu escolhi uma arquitetura baseada em componentes por várias razões:
-
Modularidade: Cada componente é autocontido com sua própria lógica e estilos
-
Reutilização: Componentes como
Card
eLoader
podem ser reutilizados em toda a aplicação -
Manutenibilidade: Mais fácil de depurar e modificar componentes individuais
-
Testes: Componentes podem ser testados de forma isolada
Organização de Componentes
- Componente Card
-
Separado em seu próprio diretório por ser um elemento fundamental do jogo
-
Contém módulos JSX e SCSS para encapsulamento
-
Gerencia a renderização individual do cartão, estados de carregamento e eventos de clique
- Componente CardsGrid
-
Gerencia o layout do tabuleiro do jogo
-
Gerencia a mistura e distribuição dos cartões
-
Controla o layout de grade responsiva 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 precise de estados de carregamento
- Componentes de Cabeçalho/Rodapé/Legenda
-
Componentes estruturais para layout do app
-
Cabeçalho exibe o título do jogo e as pontuações
-
Rodapé mostra informações de copyright e versão
-
Legenda fornece instruções do jogo
Abordagem de Módulos CSS
Usei Módulos CSS (.module.scss
arquivos) para vários benefícios:
-
Estilização Escopada: Impede vazamentos de estilo entre componentes
-
Colisões de Nomes: Gera automaticamente nomes de classes únicos
-
Manutenibilidade: Os estilos estão co-localizados com seus componentes
-
Recursos SCSS: Aproveita os recursos do 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
-
Reutilização: Pode ser usado por qualquer componente que precise de dados de imagem
-
Gerenciamento de Estado: Gerencia estados de carregamento, erro e dados
-
Desempenho: Implementa otimizações como controle de tamanho de imagem
Arquivos de Nível Raiz
App.jsx:
-
Funciona como o ponto de entrada da aplicação
-
Gerencia o estado global e o roteamento (se necessário)
-
Coordena a composição de componentes
-
Trata layouts de alto nível
Considerações de desempenho
A estrutura suporta otimizações de desempenho:
-
Divisão de código: Componentes podem ser carregados sob demanda, se necessário
-
Memoização: Componentes podem ser memoizados de forma eficaz
-
Carregamento de estilos: Módulos CSS permitem um carregamento eficiente de estilos
-
Gerenciamento de recursos: Imagens e recursos são devidamente organizados
Escalabilidade
Esta estrutura permite uma escalabilidade fácil:
-
Novos recursos podem ser adicionados como novos componentes
-
Ganchos adicionais podem ser criados para novas funcionalidades
-
Os estilos permanecem manuteníveis à medida que o aplicativo cresce
-
Testes podem ser implementados em qualquer nível
Experiência de Desenvolvimento
A estrutura melhora a experiência do desenvolvedor:
-
Organização clara de arquivos
-
Localização intuitiva dos componentes
-
Fácil localização e modificação de recursos específicos
-
Suporta colaboração eficiente
Essa arquitetura se mostrou particularmente valiosa ao otimizar o jogo para uso em tablet, pois me permitiu:
-
Identificar e otimizar facilmente gargalos de desempenho
-
Adicionar estilos específicos para tablets sem afetar outros dispositivos
-
Implementar estados de carregamento para uma melhor experiência móvel
-
Manter separação limpa entre lógica do jogo e componentes de interface do usuário
Certo, agora vamos codificar.
Guia de Construção Passo a Passo
1. Configuração do Projeto
Configurar o Ambiente de Desenvolvimento
Para começar com um projeto React limpo, abra o seu aplicativo de terminal e execute os seguintes comandos (você pode nomear a sua pasta do 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 utilizaremos neste projeto são o pacote hook da UI.dev (aliás, aqui você pode encontrar um artigo bem explicado sobre como o rendering funciona no React).
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 o 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 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 os avisos de dependência do SASS
charset: false // Impede o aviso de charset nas 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 do SASS:
-
quietDeps: true
: Isso silencia as advertências sobre dependências obsoletas em módulos SASS. Particularmente útil ao trabalhar com arquivos SASS/SCSS de terceiros. -
charset: false
: Impede a advertência “@charset” que aparece em 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 nos 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 corretamente as configurações de ambiente de teste e garantindo que o processamento de SASS/SCSS funcione sem problemas.
Você pode ver avisos no seu console sem essas configurações quando:
-
Usando recursos SASS/SCSS ou importando arquivos SASS
-
Executando testes que requerem manipulação do DOM
-
Usando caracteres especiais em suas folhas de estilo
2. Construindo os Componentes
Criar o Componente Card
Primeiro, vamos criar nosso componente básico de cartão que irá 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 de nosso jogo. É responsável por exibir imagens individuais e lidar com as interações do jogador. Vamos analisar sua implementação:
Detalhamento dos props:
-
imagem
: (string)-
A URL da imagem a ser exibida recebida do nosso serviço de API.
-
É usada diretamente no atributo src da tag img.
-
-
id
: (string)-
Identificador único para cada cartão, essencial para rastrear quais cartões foram clicados.
É passado para o callback
processTurn
quando um cartão é clicado. -
-
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)}
/>
Recursos principais:
-
Otimização de Desempenho:
-
Utiliza
React.memo
para evitar renderizações desnecessárias -
Implementa
useCallback
para manipuladores de eventos -
Gerencia o estado de carregamento internamente para uma melhor UX
-
-
Gerenciamento do Estado de Carregamento:
-
O estado interno
isLoading
acompanha o carregamento da imagem -
Mostra um componente Loader com uma mensagem durante o carregamento
-
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 dos jogadores via o 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 forem buscados
useEffect(() => {
if (fetchedData?.images) {
setImages(fetchedData.images);
setIsLoading(false);
// Reiniciar 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 central do jogo
function processTurn(imageId) {
const newClickedImages = [...clickedImages, imageId];
setClickedImages(newClickedImages);
// Se clicar na mesma imagem duas vezes, reiniciar 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 da carta
const newScore = score + 1;
setScore(newScore);
// Verificar a pontuação perfeita (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 mobile-first */
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 */
}
}
Principais Recursos:
-
Gerenciamento de Estado:
-
Utiliza
useState
para o 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
: Melhor pontuação alcançada
-
-
Gerencia o estado de carregamento para buscar imagens
-
Embaralha as cartas
-
-
Lógica do Jogo:
-
processTurn
: Gerencia as jogadas dos jogadores-
Registra cliques duplicados
-
Atualiza as pontuações
-
Gerencia cenários de pontuação perfeita
-
-
updateBestScore
: Atualiza a pontuação mais alta quando necessário -
Busca automaticamente novas imagens quando uma rodada é concluída
-
-
Busca de Dados:
-
Utiliza o hook
useFetch
personalizado para dados de imagem -
Gerencia estados de carregamento e erro
-
Atualiza as imagens quando novos dados são buscados
-
-
Otimização de Desempenho:
-
Componente envolvido em
React.memo
-
Atualizações de estado eficientes
-
Layout de grade responsivo
-
-
Persistência:
-
Estado do jogo persiste através de recarregamentos da página
-
Rastreamento do melhor score
-
Salvamento do progresso do jogo atual
-
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 de nosso jogo de memória de cartas, 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 manutenibilidade do código por meio de uma clara separação de preocupações e 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 da 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 a API primária primeiro
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 Implementação da Nossa API:
-
Múltiplas Fontes de API:
-
API Primária (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 respostas da API
-
Verifica URLs de imagem válidas
-
Fornece mensagens de erro detalhadas
-
Mecanismo de fallback gracioso
-
-
Recursos de Segurança:
-
Filtragem de conteúdo segura (
classificação=seguro
) -
Limitação de contagem de imagens (21 imagens)
-
Validação de URL
-
Validação de formato de resposta
-
-
Considerações de Desempenho:
-
Tamanhos de imagem otimizados
-
Tags de conteúdo filtradas
-
Transformação eficiente de dados
-
Chamadas mínimas de API
-
Esta implementação garante que nosso jogo tenha uma fonte confiável de imagens, lidando graciosamente com possíveis falhas de 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 Jogo de Memória, implementamos uma estratégia abrangente de testes usando ferramentas e práticas modernas. Vamos mergulhar em como estruturamos nossos testes e alguns padrões-chave de testes que utilizamos.
Pilha de Testes
-
Vitest: Nosso framework de testes principal, escolhido por sua velocidade 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
Padrões Chave de Teste
Os testes foram uma parte crucial para garantir a confiabilidade e manutenibilidade deste Jogo de Memória. Implementei uma estratégia abrangente de testes usando React Testing Library e Vitest, focando em várias áreas-chave:
1. Teste de Componentes
Escrevi testes extensivos para meus componentes React para garantir que eles são renderizados corretamente e se comportam conforme o esperado. Por exemplo, o componente CardsGrid
, que é o coração do jogo, possui uma cobertura de teste abrangente, incluindo:
-
Estados de renderização inicial
-
Estados de carregamento
-
Tratamento de erros
-
Rastreamento de pontuação
-
Comportamento de interação com cartas
2. Mock de Teste
Para garantir testes confiáveis e rápidos, implementei várias estratégias de mock:
-
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 Teste
Ao longo da minha implementação de testes, segui várias melhores práticas:
-
Utilizando os ganchos
beforeEach
eafterEach
para redefinir o estado entre os testes -
Testando interações do usuário usando o
fireEvent
da React Testing Library -
Escrevendo testes que se assemelham a como os usuários interagem com o aplicativo
-
Testando cenários de sucesso e erro
-
Isolando testes usando mocks apropriados
4. Ferramentas de Teste
O projeto utiliza ferramentas e bibliotecas modernas de teste:
-
Vitest: Como o executor de testes
-
React Testing Library: Para testar componentes React
-
@testing-library/jest-dom: Para asserções aprimoradas de teste do DOM
-
@testing-library/user-event: Para simular interações do usuário
Esta abordagem abrangente de teste me ajudou a identificar bugs precocemente, garantiu a qualidade do código e tornou a refatoração mais segura 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
-
Utilizando o modo
no-cors
quando apropriado para lidar eficientemente com problemas de CORS -
Tratamento de erro com códigos de status específicos para uma melhor depuração
-
Estrutura de resposta consistente em todas as implementações de API
-
-
Considerações Mobile-First
-
Estratégia de carregamento de imagem otimizada
-
Manuseio eficiente de erros para evitar tentativas desnecessárias
-
Transformação de dados otimizada para reduzir a sobrecarga de processamento
-
Melhorias Futuras
Existem algumas maneiras pelas quais poderíamos melhorar ainda mais este projeto:
-
Cache de Respostas de API
-
Implementar cache de armazenamento local para imagens usadas com frequência
-
Adicionar estratégia de invalidação de cache para conteúdo novo
-
Implementar carregamento de imagem progressivo
-
-
Otimizações de desempenho
-
Adicionar carregamento lento de imagens para melhor tempo de carregamento inicial
-
Implementar fila de requisições para melhor gerenciamento de largura de banda
-
Adicionar compressão de resposta para transferência de dados mais rápida
-
-
Aprimoramentos de confiabilidade
-
Adicionar verificação de saúde da API antes das tentativas
-
Implementar mecanismos de tentativa com retrocesso exponencial
-
Adicionar padrão de disjuntor para APIs com falhas
-
-
Análises e Monitoramento
-
Rastreie as taxas de sucesso da API
-
Monitore os tempos de resposta
-
Implemente troca automática de APIs 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 de rede adversas ouindisponibilidade da API, mantendo ao mesmo tempo espaço para futuras melhorias e otimizações.
Conclusão
Construir este Jogo da Memória foi mais do que criar uma alternativa divertida e sem anúncios para crianças — foi um exercício em implementar as melhores práticas de desenvolvimento web moderno enquanto se resolve um problema do mundo real.
O projeto demonstra como combinar uma arquitetura cuidadosa, testes robustos e mecanismos de fallback confiáveis pode resultar em uma aplicação pronta para produção que é ao mesmo tempo divertida e educativa.
🗝️ Principais pontos
-
Desenvolvimento Centrado no Usuário
-
Começou com um problema claro (jogos cheios de anúncios afetando a experiência do usuário)
-
Implementou recursos que aprimoram a jogabilidade sem interrupções
-
Manteve o foco no desempenho e confiabilidade em todos os dispositivos
-
-
Excelência Técnica
-
Utilizou padrões modernos do React e hooks para código limpo e manutenível
-
Implementou uma estratégia abrangente de testes garantindo confiabilidade
-
Criou um sistema robusto de fallback de API para jogabilidade ininterrupta
-
-
Performance em Primeiro Lugar
-
Adotou uma abordagem mobile-first com design responsivo
-
Otimizou o carregamento e o manuseio de imagens
-
Implementou estratégias eficientes de gerenciamento de estado e cache
-
📚 Resultados de Aprendizagem
Este projeto demonstra como jogos aparentemente simples podem ser excelentes veículos para implementar soluções técnicas complexas. Da arquitetura de componentes aos fallbacks de API, cada recurso foi construído com escalabilidade e facilidade de manutenção em mente, provando que até mesmo projetos de hobby podem manter a qualidade de código de nível profissional.
🔮 Próximos Passos
Embora o jogo alcance com sucesso seu objetivo principal de oferecer uma experiência agradável sem anúncios, as melhorias futuras documentadas fornecem um roadmap claro para evolução. Seja implementando otimizações adicionais ou adicionando novos recursos, a base é 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/