Hooks: useState e useEffect

1. Introdução aos Hooks no React

Os Hooks foram introduzidos no React 16.8 como uma forma de usar estado e outras funcionalidades do React sem escrever classes. Antes deles, componentes funcionais eram "burros" — apenas recebiam props e renderizavam JSX. Com os Hooks, tornaram-se completos, com capacidade de gerenciar estado, efeitos colaterais e contexto.

Duas regras fundamentais regem o uso de Hooks:
- Chame Hooks apenas no nível superior do seu componente, nunca dentro de loops, condicionais ou funções aninhadas
- Use Hooks apenas em componentes funcionais ou em custom Hooks, nunca em funções JavaScript comuns

Entre os Hooks mais comuns, useState e useEffect são os pilares. O primeiro gerencia estado local; o segundo lida com efeitos colaterais.

2. useState: Gerenciando Estado Local

A sintaxe básica é:

const [state, setState] = useState(initialValue);

Diferente do this.setState em classes, que fazia merge automático de objetos, o useState substitui completamente o valor anterior. Isso é importante ao trabalhar com objetos.

Exemplos práticos

Contador simples:

import React, { useState } from 'react';

function Contador() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Você clicou {count} vezes</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  );
}

Formulário com estado:

function Formulario() {
  const [nome, setNome] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ nome, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={nome} onChange={(e) => setNome(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Enviar</button>
    </form>
  );
}

Toggle de visibilidade:

function Toggle() {
  const [visivel, setVisivel] = useState(false);

  return (
    <div>
      <button onClick={() => setVisivel(!visivel)}>
        {visivel ? 'Esconder' : 'Mostrar'}
      </button>
      {visivel && <p>Conteúdo visível</p>}
    </div>
  );
}

3. useState: Padrões Avançados

Estado derivado: em vez de armazenar valores redundantes, calcule a partir do estado existente:

function ListaProdutos() {
  const [produtos, setProdutos] = useState([]);
  const total = produtos.reduce((acc, p) => acc + p.preco, 0); // derivado

  return <p>Total: R$ {total}</p>;
}

Atualizações baseadas no valor anterior: use a forma funcional de setState:

function ContadorSeguro() {
  const [count, setCount] = useState(0);

  const incrementarTres = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // cada chamada recebe o valor mais recente
  };

  return <button onClick={incrementarTres}>+3</button>;
}

Imutabilidade com objetos e arrays:

function Usuario() {
  const [usuario, setUsuario] = useState({ nome: '', idade: 0 });

  const atualizarNome = (nome) => {
    setUsuario(prev => ({ ...prev, nome })); // spread para manter imutabilidade
  };

  return <input value={usuario.nome} onChange={(e) => atualizarNome(e.target.value)} />;
}

4. useEffect: Efeitos Colaterais em Componentes

Efeitos colaterais são operações que afetam algo fora do componente: chamadas de API, timers, manipulação direta do DOM, console.log, etc.

Sintaxe básica:

useEffect(() => {
  // lógica do efeito
  return () => {
    // função de limpeza (opcional)
  };
}, [dependencias]);

O useEffect substitui três métodos do ciclo de vida de classes:
- componentDidMount — quando o array de dependências é vazio []
- componentDidUpdate — quando há dependências que mudam
- componentWillUnmount — na função de limpeza retornada

5. useEffect: Array de Dependências e Limpeza

Array vazio: executa apenas na montagem:

useEffect(() => {
  console.log('Componente montado');
}, []);

Com variáveis: executa quando a dependência muda:

const [id, setId] = useState(1);

useEffect(() => {
  console.log(`ID mudou para ${id}`);
}, [id]);

Sem array: executa em toda renderização (cuidado com loops infinitos):

useEffect(() => {
  console.log('Renderizou');
}); // sem array = executa sempre

Função de limpeza: essencial para evitar memory leaks:

function Timer() {
  const [segundos, setSegundos] = useState(0);

  useEffect(() => {
    const intervalo = setInterval(() => {
      setSegundos(prev => prev + 1);
    }, 1000);

    return () => clearInterval(intervalo); // limpa ao desmontar
  }, []);

  return <p>{segundos}s</p>;
}

6. Casos de Uso Comuns com useState e useEffect

Carregamento de dados de API com loading state:

function ListaUsuarios() {
  const [usuarios, setUsuarios] = useState([]);
  const [loading, setLoading] = useState(true);
  const [erro, setErro] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(res => res.json())
      .then(data => {
        setUsuarios(data);
        setLoading(false);
      })
      .catch(err => {
        setErro(err.message);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Carregando...</p>;
  if (erro) return <p>Erro: {erro}</p>;
  return <ul>{usuarios.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Persistência com localStorage:

function TemaEscuro() {
  const [temaEscuro, setTemaEscuro] = useState(() => {
    return localStorage.getItem('tema') === 'escuro';
  });

  useEffect(() => {
    localStorage.setItem('tema', temaEscuro ? 'escuro' : 'claro');
  }, [temaEscuro]);

  return (
    <button onClick={() => setTemaEscuro(!temaEscuro)}>
      Alternar tema
    </button>
  );
}

Debounce em busca:

function Busca() {
  const [termo, setTermo] = useState('');
  const [resultados, setResultados] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (termo) {
        fetch(`https://api.exemplo.com/busca?q=${termo}`)
          .then(res => res.json())
          .then(data => setResultados(data));
      }
    }, 500);

    return () => clearTimeout(timer); // cancela busca anterior
  }, [termo]);

  return (
    <div>
      <input value={termo} onChange={(e) => setTermo(e.target.value)} />
      <ul>{resultados.map(r => <li key={r.id}>{r.nome}</li>)}</ul>
    </div>
  );
}

7. Boas Práticas e Armadilhas

Evitar loops infinitos: sempre especifique dependências corretas no useEffect. Se um efeito modifica o estado que está em suas dependências, pode causar re-renderizações infinitas.

Nunca modifique estado diretamente:

// ERRADO
const [itens, setItens] = useState([1, 2, 3]);
itens.push(4); // mutação direta

// CERTO
setItens([...itens, 4]); // novo array

Separar lógica em custom Hooks:

function useLocalStorage(chave, valorInicial) {
  const [valor, setValor] = useState(() => {
    const salvo = localStorage.getItem(chave);
    return salvo ? JSON.parse(salvo) : valorInicial;
  });

  useEffect(() => {
    localStorage.setItem(chave, JSON.stringify(valor));
  }, [chave, valor]);

  return [valor, setValor];
}

// Uso
function App() {
  const [nome, setNome] = useLocalStorage('nome', '');
  return <input value={nome} onChange={(e) => setNome(e.target.value)} />;
}

8. Conclusão e Próximos Passos

useState e useEffect são a base para construir componentes React modernos. Com useState você gerencia estado local de forma declarativa; com useEffect você sincroniza seu componente com o mundo exterior — APIs, timers, localStorage, etc.

Lembre-se: estado local é para dados que pertencem a um único componente. Quando precisar compartilhar estado entre múltiplos componentes, considere usar Context API (useContext) ou bibliotecas como Redux.

Exercício prático: construa uma lista de tarefas onde:
- useState gerencia a lista de tarefas e o input do usuário
- useEffect salva a lista no localStorage sempre que ela mudar
- Ao recarregar a página, as tarefas persistem

function ListaTarefas() {
  const [tarefas, setTarefas] = useState(() => {
    const salvas = localStorage.getItem('tarefas');
    return salvas ? JSON.parse(salvas) : [];
  });
  const [input, setInput] = useState('');

  useEffect(() => {
    localStorage.setItem('tarefas', JSON.stringify(tarefas));
  }, [tarefas]);

  const adicionar = () => {
    if (input.trim()) {
      setTarefas([...tarefas, { id: Date.now(), texto: input, concluida: false }]);
      setInput('');
    }
  };

  const toggle = (id) => {
    setTarefas(tarefas.map(t => t.id === id ? { ...t, concluida: !t.concluida } : t));
  };

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={adicionar}>Adicionar</button>
      <ul>
        {tarefas.map(t => (
          <li key={t.id} onClick={() => toggle(t.id)} style={{ textDecoration: t.concluida ? 'line-through' : 'none' }}>
            {t.texto}
          </li>
        ))}
      </ul>
    </div>
  );
}

Dominar esses dois Hooks é o primeiro passo para criar aplicações React robustas e reutilizáveis.

Referências