Tratamento de Eventos em React

Módulo 4: Eventos e Formulários

Aula 1
1

Sistema de Eventos do React

15:00

Entenda como funcionam os eventos no React

2

Exemplos Práticos de Eventos

Veja como implementar diferentes tipos de eventos

Exemplos Práticos de Eventos

Eventos de Mouse

Click e Double Click

function BotoesInterativos() {
  const [cliques, setCliques] = useState(0);
  const [duplos, setDuplos] = useState(0);

  const handleClick = () => {
    setCliques(cliques + 1);
  };

  const handleDoubleClick = () => {
    setDuplos(duplos + 1);
  };

  return (
    <div>
      <button onClick={handleClick}>Clique: {cliques}</button>
      <button onDoubleClick={handleDoubleClick}>Duplo: {duplos}</button>
    </div>
  );
}

Hover (Mouse Enter/Leave)

function CardHover() {
  const [hovering, setHovering] = useState(false);

  return (
    <div
      onMouseEnter={() => setHovering(true)}
      onMouseLeave={() => setHovering(false)}
      style={{
        padding: '20px',
        backgroundColor: hovering ? '#e0e0e0' : '#f5f5f5',
        transition: 'background-color 0.3s'
      }}
    >
      {hovering ? 'Mouse sobre o card!' : 'Passe o mouse aqui'}
    </div>
  );
}

Eventos de Teclado

Detectando Teclas Específicas

function DetectorTeclas() {
  const [ultimaTecla, setUltimaTecla] = useState('');
  const [enter, setEnter] = useState(0);

  const handleKeyDown = (e) => {
    setUltimaTecla(e.key);
    
    if (e.key === 'Enter') {
      setEnter(enter + 1);
    }
    
    // Prevenir comportamento padrão para algumas teclas
    if (e.key === 'Tab') {
      e.preventDefault();
      console.log('Tab foi prevenido');
    }
  };

  return (
    <div>
      <input
        type="text"
        onKeyDown={handleKeyDown}
        placeholder="Digite algo..."
      />
      <p>Última tecla: {ultimaTecla}</p>
      <p>Enter pressionado: {enter} vezes</p>
    </div>
  );
}

Atalhos de Teclado

function AtalhosTeclado() {
  const [acao, setAcao] = useState('');

  useEffect(() => {
    const handleKeyPress = (e) => {
      // Ctrl/Cmd + S
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();
        setAcao('Salvando...');
        setTimeout(() => setAcao(''), 2000);
      }
      
      // Ctrl/Cmd + K
      if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
        e.preventDefault();
        setAcao('Abrindo busca...');
      }
    };

    window.addEventListener('keydown', handleKeyPress);
    return () => window.removeEventListener('keydown', handleKeyPress);
  }, []);

  return (
    <div>
      <p>Tente Ctrl+S ou Ctrl+K</p>
      {acao && <p style={{ color: 'green' }}>{acao}</p>}
    </div>
  );
}

Passando Parâmetros para Handlers

Usando Arrow Functions

function ListaItens() {
  const itens = ['Maçã', 'Banana', 'Laranja'];
  const [selecionado, setSelecionado] = useState('');

  const handleClick = (item) => {
    setSelecionado(item);
  };

  return (
    <div>
      <ul>
        {itens.map(item => (
          <li key={item}>
            <button onClick={() => handleClick(item)}>
              {item}
            </button>
          </li>
        ))}
      </ul>
      <p>Selecionado: {selecionado}</p>
    </div>
  );
}

Usando o Evento

function FormularioDinamico() {
  const [valores, setValores] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValores(prev => ({
      ...prev,
      [name]: value
    }));
  };

  return (
    <form>
      <input
        name="nome"
        placeholder="Nome"
        onChange={handleChange}
      />
      <input
        name="email"
        placeholder="Email"
        onChange={handleChange}
      />
      <pre>{JSON.stringify(valores, null, 2)}</pre>
    </form>
  );
}

Propagação de Eventos

Stopando Propagação

function PropagacaoEventos() {
  const handleDivClick = () => {
    console.log('Div clicada');
  };

  const handleButtonClick = (e) => {
    e.stopPropagation(); // Impede que o evento suba para a div
    console.log('Botão clicado');
  };

  return (
    <div 
      onClick={handleDivClick}
      style={{ padding: '50px', backgroundColor: '#f0f0f0' }}
    >
      <button onClick={handleButtonClick}>
        Clique (sem propagar)
      </button>
    </div>
  );
}

Eventos Customizados

Criando e Disparando

function ComponenteComEventoCustom() {
  useEffect(() => {
    const handleCustomEvent = (e) => {
      console.log('Evento customizado recebido:', e.detail);
    };

    window.addEventListener('meuEventoCustom', handleCustomEvent);

    return () => {
      window.removeEventListener('meuEventoCustom', handleCustomEvent);
    };
  }, []);

  const dispararEvento = () => {
    const evento = new CustomEvent('meuEventoCustom', {
      detail: { mensagem: 'Olá do React!' }
    });
    window.dispatchEvent(evento);
  };

  return (
    <button onClick={dispararEvento}>
      Disparar Evento Customizado
    </button>
  );
}

Performance com Eventos

Debouncing

function CampoBusca() {
  const [busca, setBusca] = useState('');
  const [resultados, setResultados] = useState([]);

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      if (busca) {
        console.log('Buscando:', busca);
        // Simular busca
        setResultados([`Resultado para "${busca}"`]);
      }
    }, 500);

    return () => clearTimeout(timeoutId);
  }, [busca]);

  return (
    <div>
      <input
        type="text"
        value={busca}
        onChange={(e) => setBusca(e.target.value)}
        placeholder="Digite para buscar..."
      />
      <ul>
        {resultados.map((r, i) => <li key={i}>{r}</li>)}
      </ul>
    </div>
  );
}

Throttling

function ScrollInfinito() {
  const [scrolls, setScrolls] = useState(0);
  const [canScroll, setCanScroll] = useState(true);

  const handleScroll = () => {
    if (!canScroll) return;
    
    setCanScroll(false);
    setScrolls(prev => prev + 1);
    
    setTimeout(() => {
      setCanScroll(true);
    }, 200); // Throttle de 200ms
  };

  return (
    <div
      onScroll={handleScroll}
      style={{ height: '200px', overflow: 'auto' }}
    >
      <div style={{ height: '500px' }}>
        <p>Scroll detectado: {scrolls} vezes</p>
        <p>Role para baixo...</p>
      </div>
    </div>
  );
}

Boas Práticas

  1. Use funções nomeadas para handlers complexos
  2. Evite criar funções inline em renders (performance)
  3. Sempre limpe listeners em useEffect
  4. Use preventDefault() quando necessário
  5. Considere debounce/throttle para eventos frequentes
3

Exercício: Jogo de Cliques

Crie um jogo interativo usando diferentes eventos

Activity

Exercício: Jogo de Cliques

Objetivo

Criar um jogo onde o usuário deve clicar em alvos que aparecem aleatoriamente na tela.

Requisitos

  1. Alvo Móvel

    • Quadrado ou círculo que aparece em posições aleatórias
    • Desaparece após 2 segundos
    • Reaparece em nova posição
  2. Sistema de Pontuação

    • +10 pontos por acerto
    • -5 pontos por erro (clicar fora)
    • Mostrar pontuação atual
  3. Níveis de Dificuldade

    • Fácil: alvo grande, 3 segundos
    • Médio: alvo médio, 2 segundos
    • Difícil: alvo pequeno, 1 segundo
  4. Recursos Extras

    • Contador de tempo
    • Recorde de pontuação
    • Botão para pausar/continuar

Template Inicial

import React, { useState, useEffect } from 'react';

function JogoDeCliques() {
  const [posicao, setPosicao] = useState({ x: 0, y: 0 });
  const [pontos, setPontos] = useState(0);
  const [ativo, setAtivo] = useState(false);
  const [dificuldade, setDificuldade] = useState('medio');

  const configuracoes = {
    facil: { tamanho: 80, tempo: 3000 },
    medio: { tamanho: 60, tempo: 2000 },
    dificil: { tamanho: 40, tempo: 1000 }
  };

  // Seu código aqui

  return (
    <div>
      {/* Implemente a interface aqui */}
    </div>
  );
}

export default JogoDeCliques;

Dicas

  • Use Math.random() para posições aleatórias
  • getBoundingClientRect() para limites da área de jogo
  • setTimeout para controlar aparição/desaparecimento
  • Considere usar position: absolute para o alvo

Solução

Clique para ver a solução
import React, { useState, useEffect, useRef } from 'react';

function JogoDeCliques() {
  const [posicao, setPosicao] = useState({ x: 0, y: 0 });
  const [pontos, setPontos] = useState(0);
  const [ativo, setAtivo] = useState(false);
  const [visivel, setVisivel] = useState(false);
  const [dificuldade, setDificuldade] = useState('medio');
  const [tempo, setTempo] = useState(0);
  const [recorde, setRecorde] = useState(
    parseInt(localStorage.getItem('recorde') || '0')
  );
  
  const areaJogoRef = useRef(null);
  const timeoutRef = useRef(null);
  const intervalRef = useRef(null);

  const configuracoes = {
    facil: { tamanho: 80, tempo: 3000, cor: '#4caf50' },
    medio: { tamanho: 60, tempo: 2000, cor: '#ff9800' },
    dificil: { tamanho: 40, tempo: 1000, cor: '#f44336' }
  };

  const config = configuracoes[dificuldade];

  const gerarPosicaoAleatoria = () => {
    if (!areaJogoRef.current) return { x: 0, y: 0 };
    
    const area = areaJogoRef.current.getBoundingClientRect();
    const x = Math.random() * (area.width - config.tamanho);
    const y = Math.random() * (area.height - config.tamanho);
    
    return { x, y };
  };

  const mostrarAlvo = () => {
    setPosicao(gerarPosicaoAleatoria());
    setVisivel(true);
    
    timeoutRef.current = setTimeout(() => {
      setVisivel(false);
      if (ativo) {
        setTimeout(mostrarAlvo, 500);
      }
    }, config.tempo);
  };

  const handleClickAlvo = (e) => {
    e.stopPropagation();
    setPontos(prev => prev + 10);
    setVisivel(false);
    
    clearTimeout(timeoutRef.current);
    
    if (ativo) {
      setTimeout(mostrarAlvo, 300);
    }
  };

  const handleClickFora = () => {
    if (ativo && visivel) {
      setPontos(prev => Math.max(0, prev - 5));
    }
  };

  const iniciarJogo = () => {
    setAtivo(true);
    setPontos(0);
    setTempo(0);
    mostrarAlvo();
  };

  const pararJogo = () => {
    setAtivo(false);
    setVisivel(false);
    clearTimeout(timeoutRef.current);
    clearInterval(intervalRef.current);
    
    if (pontos > recorde) {
      setRecorde(pontos);
      localStorage.setItem('recorde', pontos.toString());
    }
  };

  useEffect(() => {
    if (ativo) {
      intervalRef.current = setInterval(() => {
        setTempo(prev => prev + 1);
      }, 1000);
    }
    
    return () => {
      clearInterval(intervalRef.current);
    };
  }, [ativo]);

  useEffect(() => {
    return () => {
      clearTimeout(timeoutRef.current);
      clearInterval(intervalRef.current);
    };
  }, []);

  return (
    <div style={{ 
      maxWidth: '800px', 
      margin: '0 auto', 
      padding: '20px',
      textAlign: 'center'
    }}>
      <h1>Jogo de Cliques</h1>
      
      <div style={{ 
        display: 'flex', 
        justifyContent: 'space-around', 
        marginBottom: '20px' 
      }}>
        <div>Pontos: {pontos}</div>
        <div>Tempo: {tempo}s</div>
        <div>Recorde: {recorde}</div>
      </div>

      {!ativo && (
        <div style={{ marginBottom: '20px' }}>
          <label>Dificuldade: </label>
          <select 
            value={dificuldade} 
            onChange={(e) => setDificuldade(e.target.value)}
          >
            <option value="facil">Fácil</option>
            <option value="medio">Médio</option>
            <option value="dificil">Difícil</option>
          </select>
        </div>
      )}

      <button 
        onClick={ativo ? pararJogo : iniciarJogo}
        style={{ 
          marginBottom: '20px',
          padding: '10px 20px',
          fontSize: '16px'
        }}
      >
        {ativo ? 'Parar Jogo' : 'Iniciar Jogo'}
      </button>

      <div
        ref={areaJogoRef}
        onClick={handleClickFora}
        style={{
          position: 'relative',
          width: '100%',
          height: '400px',
          border: '2px solid #333',
          backgroundColor: '#f5f5f5',
          cursor: ativo ? 'crosshair' : 'default'
        }}
      >
        {visivel && (
          <div
            onClick={handleClickAlvo}
            style={{
              position: 'absolute',
              left: `${posicao.x}px`,
              top: `${posicao.y}px`,
              width: `${config.tamanho}px`,
              height: `${config.tamanho}px`,
              backgroundColor: config.cor,
              borderRadius: '50%',
              cursor: 'pointer',
              transition: 'all 0.1s'
            }}
          />
        )}
      </div>

      {!ativo && pontos > 0 && (
        <div style={{ marginTop: '20px' }}>
          <h3>Fim de Jogo!</h3>
          <p>Pontuação final: {pontos}</p>
          {pontos === recorde && <p style={{ color: 'green' }}>Novo recorde!</p>}
        </div>
      )}
    </div>
  );
}

export default JogoDeCliques;

Desafios Extras

  1. Power-ups: Alvos especiais que dão mais pontos
  2. Combos: Bônus por acertos consecutivos
  3. Sons: Adicionar efeitos sonoros
  4. Ranking: Sistema de ranking online
  5. Modos de Jogo: Tempo limitado, vidas limitadas, etc.
3 content items