DevTools

Cheatsheet React

Biblioteca JavaScript para interfaces

Voltar às linguagens
React
122 cards encontrados
Categorias:
Versões:

Componentes


16 cards
Componente funcional
function Ola() {
  return <h1>Olá Mundo!</h1>;
}

export default Ola;

Um componente funcional é uma função JavaScript que retorna JSX. É a forma padrão de criar componentes no React moderno.

Composição de componentes
function App() {
  return (
    <div>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

Compor é montar a interface juntando componentes menores dentro de outros. Cada componente fica simples e reutilizável.

Spread de props
const props = { nome: "Ana", idade: 30 };

// Passar todas as props de uma vez:
<Perfil {...props} />

// Equivale a:
<Perfil nome="Ana" idade={30} />

O spread (...) espalha um objeto como props individuais. Útil para repassar props sem as listar uma a uma.

Renderizar com &&
function Notificacoes({ count }) {
  return (
    <div>
      <h1>Mensagens</h1>
      {count > 0 && (
        <span className="badge">{count}</span>
      )}
    </div>
  );
}

O operador && renderiza o lado direito só se a condição for verdadeira. Mais curto que um ternário quando não há alternativa.

Componente com arrow function
const Ola = () => <h1>Olá Mundo!</h1>;

export default Ola;

Versão mais curta com arrow function. Muito usada para componentes simples de uma linha.

Atributos em JSX
<input
  type="text"
  placeholder="Nome"
  disabled={true}
  tabIndex={1}
  data-id="campo-nome"
/>

Os atributos seguem o padrão HTML, mas com nomes em camelCase (tabIndex) e valores dinâmicos entre { }.

Componente como variável
function App({ tipo }) {
  const Componente = tipo === "admin"
    ? PainelAdmin
    : PainelUser;

  return <Componente />;
}

Podes guardar um componente numa variável (com maiúscula) e renderizá-lo dinamicamente. O nome deve começar por letra maiúscula.

Componente puro (memo)
const Item = React.memo(function Item({ nome }) {
  console.log("renderizou:", nome);
  return <li>{nome}</li>;
});

// Só re-renderiza se "nome" mudar
// Comparação rasa (shallow) das props

Um componente puro (React.memo) só re-renderiza quando as props mudam. Evita renders desnecessários em listas grandes.

JSX básico
function Cartao() {
  return (
    <div className="cartao">
      <h2>Título</h2>
      <p>Conteúdo aqui</p>
      <img src="/foto.jpg" alt="Foto" />
    </div>
  );
}

JSX parece HTML, mas usa className em vez de class. Um componente deve retornar um único elemento raiz.

Renderizar no DOM
import { createRoot } from "react-dom/client";
import App from "./App";

const root = createRoot(
  document.getElementById("root")
);

root.render(<App />);

O createRoot liga o React a um elemento HTML (normalmente uma div#root). O render desenha o componente principal na página.

Early return
function Perfil({ user }) {
  if (!user) return <p>Faz login</p>;
  if (user.bloqueado) return <p>Conta suspensa</p>;

  return (
    <div>
      <h1>{user.nome}</h1>
      <p>{user.email}</p>
    </div>
  );
}

O early return trata casos especiais no início com return antecipado. O código principal fica limpo, sem ternários aninhados.

Fragment
function Linha() {
  return (
    <>
      <td>Nome</td>
      <td>Valor</td>
    </>
  );
}

// Equivalente:
import { Fragment } from "react";
return <Fragment>...</Fragment>;

O Fragment (<>...</>) agrupa elementos sem criar um nó extra no DOM. Essencial quando um componente precisa retornar múltiplos irmãos.

Expressões em JSX
function Perfil({ nome, idade }) {
  return (
    <div>
      <p>{nome}</p>
      <p>{idade + 1} anos</p>
      <p>{"texto".toUpperCase()}</p>
    </div>
  );
}

Usa { } para inserir qualquer expressão JavaScript dentro do JSX: variáveis, cálculos ou chamadas de funções.

Comentários em JSX
function Cartao() {
  return (
    <div>
      {/* Isto é um comentário em JSX */}
      <h2>Título</h2>
      {/* <p>Texto removido</p> */}
    </div>
  );
}

Dentro do JSX, os comentários usam {/* ... */}. Fora do JSX (no JavaScript), usa // normalmente.

Props com TypeScript
interface Props {
  nome: string;
  idade?: number;  // opcional
  onSalvar: (id: number) => void;
}

function Perfil({ nome, idade = 0, onSalvar }: Props) {
  return <p>{nome}, {idade}</p>;
}

Com TypeScript, defines uma interface para as props. O compilador avisa se faltar uma prop obrigatória ou o tipo estiver errado.

Children prop
function Card({ titulo, children }) {
  return (
    <div className="card">
      <h2>{titulo}</h2>
      <div className="corpo">{children}</div>
    </div>
  );
}

// Uso - o conteúdo entre as tags vira children:
<Card titulo="Perfil">
  <p>Nome: Ana</p>
  <button>Editar</button>
</Card>

A prop children recebe tudo o que é colocado entre as tags de abertura e fecho do componente. Ideal para wrappers como cards, modais e layouts.

Props e Estado


17 cards
Props
function Saudacao({ nome, idade }) {
  return <p>Olá {nome}, {idade} anos</p>;
}

<Saudacao nome="Ana" idade={30} />

Props são os dados passados de um componente pai para um filho. Funcionam como argumentos de uma função.

Estado com array
const [itens, setItens] = useState([]);

// Adicionar:
setItens([...itens, novoItem]);

// Remover:
setItens(itens.filter(i => i.id !== id));

// Atualizar:
setItens(itens.map(i =>
  i.id === id ? { ...i, nome: "novo" } : i
));

Arrays de estado são atualizados de forma imutável: cria um novo array com filter, map ou spread, em vez de alterar o original.

Atualização assíncrona
const [count, setCount] = useState(0);

// ❌ Usa o valor antigo:
setCount(count + 1);
setCount(count + 1);  // +1, não +2!

// ✅ Forma funcional (correta):
setCount(prev => prev + 1);
setCount(prev => prev + 1);  // +2 ✓

O estado é assíncrono: dentro do mesmo evento, o valor não muda imediatamente. Usa a forma funcional prev => para atualizações encadeadas.

Controlled vs Uncontrolled
// Controlled: React controla o valor
const [valor, setValor] = useState("");
<input value={valor} onChange={e => setValor(e.target.value)} />

// Uncontrolled: DOM controla (com ref)
const ref = useRef();
<input ref={ref} defaultValue="texto" />
// Ler: ref.current.value

No controlled, o React é a fonte de verdade (value + onChange). No uncontrolled, o DOM guarda o valor e lês com ref. Prefere controlled.

Estado vs Props
function ContadorPai() {
  const [total, setTotal] = useState(0);  // ESTADO (dono)

  return (
    <>
      <Mostrador valor={total} />          {/* PROP (só leitura) */}
      <button onClick={() => setTotal(total + 1)}>
        +1
      </button>
    </>
  );
}

function Mostrador({ valor }) {
  // valor é uma PROP - não pode ser modificada aqui
  return <p>Total: {valor}</p>;
}

Estado (useState) é dados que o componente controla e pode mudar. Props são dados recebidos do pai, só de leitura. O dono do estado passa-o como prop.

Props são só de leitura
function Botao({ texto }) {
  // texto = "novo";  ❌ Erro!
  return <button>{texto}</button>;
}

Uma prop nunca deve ser modificada dentro do componente que a recebe. Para dados que mudam, usa o estado.

Lifting state up
function Pai() {
  const [valor, setValor] = useState("");
  return (
    <Filho valor={valor} onChange={setValor} />
  );
}

function Filho({ valor, onChange }) {
  return (
    <input
      value={valor}
      onChange={e => onChange(e.target.value)}
    />
  );
}

Quando dois componentes precisam do mesmo estado, sobe-o para o pai comum e passa-o para baixo via props.

Estado derivado
const [itens, setItens] = useState([]);

// ✅ Calcula durante o render:
const total = itens.reduce((s, i) => s + i.preco, 0);
const ativos = itens.filter(i => i.ativo);

// ❌ Não guardes em estado:
// const [total, setTotal] = useState(0);

Se um valor pode ser calculado a partir de props ou estado existente, não o guardes em estado separado. Calcula-o diretamente no render.

Callback props
// Pai passa função como prop:
function Pai() {
  const [itens, setItens] = useState([]);
  return (
    <Filho onAdicionar={(item) =>
      setItens(prev => [...prev, item])
    } />
  );
}

// Filho chama a callback:
function Filho({ onAdicionar }) {
  return (
    <button onClick={() => onAdicionar("novo")}>
      Adicionar
    </button>
  );
}

O padrão callback props permite ao filho comunicar com o pai. O filho chama a função e o pai decide o que fazer com os dados.

useState
import { useState } from "react";

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clique: {count}
    </button>
  );
}

O useState cria uma variável de estado e a função para a atualizar. Quando o estado muda, o componente re-renderiza.

Destructuring de props
// Sem destructuring:
function Perfil(props) {
  return <p>{props.nome}, {props.idade}</p>;
}

// Com destructuring (mais limpo):
function Perfil({ nome, idade }) {
  return <p>{nome}, {idade}</p>;
}

O destructuring extrai as props diretamente nos parâmetros. Torna o código mais curto e legível, sobretudo com muitas props.

Imutabilidade
const [user, setUser] = useState({ nome: "Ana", idade: 30 });

// ❌ Muta o objeto (não re-renderiza!)
user.nome = "Rui";
setUser(user);

// ✅ Cria novo objeto
setUser({ ...user, nome: "Rui" });

// ✅ Array: novo array
setItens([...itens, novo]);

O React só re-renderiza se a referência mudar. Muta o objeto e passa a mesma referência = nada acontece. Cria sempre um novo com spread.

Default props
function Botao({ cor = "azul", tamanho = "md", children }) {
  return (
    <button className={`btn btn-${cor} btn-${tamanho}`}>
      {children}
    </button>
  );
}

// Uso:
<Botao>Padrão</Botao>           // azul, md
<Botao cor="vermelho">Erro</Botao> // vermelho, md

Use destructuring com defaults para definir valores padrão nas props. O componente funciona mesmo sem passar todas as props.

Estado com objeto
const [form, setForm] = useState({
  nome: "",
  email: ""
});

setForm(prev => ({
  ...prev,
  nome: "Ana"
}));

Para objetos, usa o spread (...) para copiar os campos existentes e atualizar apenas alguns. Nunca mutes o objeto diretamente.

Prop drilling
// O tema passa por vários níveis sem ser usado:
function App() {
  const [tema, setTema] = useState("dark");
  return <Painel tema={tema} />;
}
function Painel({ tema }) {
  return <Botao tema={tema} />;  // só repassa
}
function Botao({ tema }) {
  return <button className={tema} />;
}

Prop drilling é passar props por vários componentes que só as repassam. Para muitos níveis, usa o Context (ver Padrões Avançados).

Estado com função (lazy init)
// ❌ Calcula em todos os renders:
const [dados, setDados] = useState(
  JSON.parse(localStorage.getItem("x"))
);

// ✅ Calcula só uma vez:
const [dados, setDados] = useState(
  () => JSON.parse(localStorage.getItem("x"))
);

Passa uma função ao useState quando o valor inicial é caro de calcular. O React só a executa no primeiro render (lazy initialization).

useReducer (estado complexo)
import { useReducer } from "react";

const inicial = { count: 0, step: 1 };

function reducer(estado, acao) {
  switch (acao.type) {
    case "incrementar":
      return { ...estado, count: estado.count + estado.step };
    case "reset":
      return inicial;
    default:
      return estado;
  }
}

function Contador() {
  const [estado, dispatch] = useReducer(reducer, inicial);
  return (
    <button onClick={() => dispatch({ type: "incrementar" })}>
      {estado.count}
    </button>
  );
}

useReducer é uma alternativa ao useState para estado com lógica complexa. Um reducer centraliza as transições e o dispatch envia ações.

Hooks


20 cards
useEffect
import { useEffect } from "react";

useEffect(() => {
  document.title = `Cliques: ${count}`;

  return () => {
    // cleanup (opcional)
  };
}, [count]);

O useEffect executa código após o render. O array de dependências controla quando volta a correr.

useRef
import { useRef } from "react";

function Foco() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        Focar
      </button>
    </>
  );
}

O useRef guarda um valor persistente que não causa re-render. Muito usado para aceder diretamente a elementos do DOM.

useDeferredValue
import { useDeferredValue } from "react";

function Pesquisa({ query }) {
  const deferred = useDeferredValue(query);
  const resultados = useMemo(
    () => lista.filter(i => i.includes(deferred)),
    [deferred]
  );
  return <Lista itens={resultados} />;
}

O useDeferredValue adia a atualização de um valor, mantendo a UI responsiva. A lista atualiza logo, os resultados vêm depois.

useEffect com AbortController
useEffect(() => {
  const controller = new AbortController();

  fetch("/api/dados", {
    signal: controller.signal,
  })
    .then(r => r.json())
    .then(setDados)
    .catch(err => {
      if (err.name !== "AbortError") throw err;
    });

  return () => controller.abort();
}, [id]);

O AbortController cancela pedidos pendentes quando o componente desmonta ou a dependência muda. Evita memory leaks e race conditions.

useSyncExternalStore
import { useSyncExternalStore } from "react";

// Subscrever uma store externa (ex.: localStorage):
function useLocalStorage(key) {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("storage", callback);
      return () => window.removeEventListener("storage", callback);
    },
    () => localStorage.getItem(key)
  );
}

// Uso:
function Perfil() {
  const nome = useLocalStorage("nome");
  return <p>Olá, {nome}</p>;
}

useSyncExternalStore subscreve fontes de dados externas ao React (stores, browser APIs). Garante leituras consistentes sem tearing.

useEffect: buscar dados
const [dados, setDados] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch("/api/produtos")
    .then(r => r.json())
    .then(data => {
      setDados(data);
      setLoading(false);
    });
}, []);

Com o array de dependências vazio [], o efeito corre apenas uma vez, quando o componente monta. Ideal para carregar dados.

useMemo
import { useMemo } from "react";

const total = useMemo(
  () => itens.reduce((s, i) => s + i.preco, 0),
  [itens]
);

O useMemo memoriza o resultado de um cálculo pesado e só o recalcula quando as dependências mudam. Evita trabalho repetido.

useImperativeHandle
const Modal = forwardRef((props, ref) => {
  const [aberto, setAberto] = useState(false);

  useImperativeHandle(ref, () => ({
    abrir: () => setAberto(true),
    fechar: () => setAberto(false),
  }));

  return aberto ? <div>Modal</div> : null;
});

// No pai: modalRef.current.abrir();

O useImperativeHandle expõe métodos personalizados do filho para o pai via ref. O pai chama ref.current.abrir() diretamente.

Custom hook: useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(d => { setData(d); setLoading(false); })
      .catch(e => { setError(e); setLoading(false); });
  }, [url]);

  return { data, loading, error };
}

// Uso: const { data, loading } = useFetch("/api/x");

Um useFetch encapsula o padrão de data fetching. Devolve data, loading e error. Reutilizável em qualquer componente.

Regras dos Hooks
// CERTO - no topo do componente:
function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  useEffect(() => { }, []);
  // ...
}

// ERRADO - dentro de condição:
function App() {
  if (logado) {
    const [x, setX] = useState(0);  // ❌ ERRO
  }
  for (...) {
    useEffect(() => { });           // ❌ ERRO
  }
}

Hooks devem ser chamados sempre na mesma ordem, no topo do componente. Nunca dentro de condições, loops ou funções aninhadas. O React depende da ordem de chamada.

useEffect: dependências
// Corre em todos os renders:
useEffect(() => { ... });

// Corre só ao montar:
useEffect(() => { ... }, []);

// Corre quando "id" muda:
useEffect(() => { ... }, [id]);

O array de dependências decide quando o efeito executa: sem array corre sempre, vazio corre uma vez, com valores corre quando esses valores mudam.

useCallback
import { useCallback } from "react";

const handleClick = useCallback(
  (id) => setSelecionado(id),
  []
);

O useCallback memoriza uma função para que a referência seja estável entre renders. Útil ao passar funções para filhos otimizados.

useEffect vs useLayoutEffect
// useEffect: após o browser pintar (assíncrono)
useEffect(() => {
  console.log("depois do paint");
}, []);

// useLayoutEffect: antes do paint (síncrono)
useLayoutEffect(() => {
  // Medir DOM antes de o utilizador ver
  const rect = ref.current.getBoundingClientRect();
  setPosicao(rect.top);
}, []);

O useLayoutEffect corre antes do browser pintar. Usa-o para medições do DOM que precisam de estar prontas antes do render visual.

useId
import { useId } from "react";

function Campo({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

// Cada instância gera um ID único: ":r1:", ":r2:"...

useId() gera IDs únicos e estáveis para acessibilidade (label + input). Nunca usar Math.random() para IDs de acessibilidade.

Hook use() (React 19)
import { use, Suspense } from "react";

// O use() lê uma promise dentro do render:
function Perfil({ userPromise }) {
  const user = use(userPromise);  // suspende até resolver
  return <p>{user.nome}</p>;
}

// Envolve com Suspense:
<Suspense fallback={<Spinner />}>
  <Perfil userPromise={fetchUser()} />
</Suspense>

O use() (React 19) lê o valor de uma promise diretamente no render, suspendendo o componente até ela resolver. Ao contrário do useContext, pode ser chamado dentro de condições.

useEffect: cleanup
useEffect(() => {
  const timer = setInterval(() => {
    setSegundos(s => s + 1);
  }, 1000);

  return () => clearInterval(timer);
}, []);

A função retornada faz a limpeza antes do componente desmontar ou do efeito repetir. Essencial para timers, listeners e subscrições.

Custom Hook
function useLocalStorage(key, inicial) {
  const [valor, setValor] = useState(
    () => JSON.parse(localStorage.getItem(key)) ?? inicial
  );

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

  return [valor, setValor];
}

Um custom hook é uma função que começa por use e reutiliza lógica com estado entre vários componentes.

useDebugValue
function useOnline() {
  const [online, setOnline] = useState(navigator.onLine);

  useDebugValue(online ? "Online" : "Offline");

  return online;
}

// Visível no React DevTools:
// useOnline: "Online"

O useDebugValue mostra um valor personalizado no React DevTools para custom hooks. Facilita a depuração sem console.log.

useTransition
import { useTransition, useState } from "react";

function Pesquisa() {
  const [isPending, startTransition] = useTransition();
  const [resultados, setResultados] = useState([]);

  const pesquisar = (texto) => {
    startTransition(() => {
      setResultados(filtrar(texto)); // não bloqueia o input
    });
  };

  return (
    <>
      <input onChange={e => pesquisar(e.target.value)} />
      {isPending ? <Spinner /> : <Lista itens={resultados} />}
    </>
  );
}

useTransition marca atualizações como não-urgentes. O input mantém-se responsivo enquanto a lista pesada atualiza em background.

useOptimistic (React 19)
import { useOptimistic } from "react";

function Likes({ total, gostar }) {
  const [otimista, setOtimista] = useOptimistic(total);

  async function handleClick() {
    setOtimista(otimista + 1);  // mostra já, antes da resposta
    await gostar();             // o estado real vem do servidor
  }

  return (
    <button onClick={handleClick}>
      Gosto: {otimista}
    </button>
  );
}

O useOptimistic (React 19) mostra de imediato o resultado esperado, antes de o servidor responder. Se o pedido falhar, o valor volta automaticamente ao estado real.

Eventos e Formulários


15 cards
Event handlers
function Botao() {
  const handleClick = (e) => {
    e.preventDefault();
    console.log("clicado!");
  };

  return (
    <button onClick={handleClick}>
      Clicar
    </button>
  );
}

Os eventos usam nomes em camelCase (onClick, onChange, onSubmit) e recebem uma função, não uma string.

Checkbox
const [aceite, setAceite] = useState(false);

<label>
  <input
    type="checkbox"
    checked={aceite}
    onChange={e => setAceite(e.target.checked)}
  />
  Aceito os termos
</label>

No checkbox usa checked (não value) e lê e.target.checked para saber se ficou marcado ou desmarcado.

Upload de ficheiros
function Upload() {
  const [ficheiro, setFicheiro] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append("ficheiro", ficheiro);
    await fetch("/api/upload", {
      method: "POST",
      body: formData,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file"
        onChange={e => setFicheiro(e.target.files[0])} />
      <button>Enviar</button>
    </form>
  );
}

Para upload, usa FormData e o input type="file". Não definas Content-Type — o browser define o multipart automaticamente.

Vários inputs com name
function Form() {
  const [dados, setDados] = useState({
    nome: "", email: "", cidade: ""
  });

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

  return (
    <>
      <input name="nome" value={dados.nome} onChange={handleChange} />
      <input name="email" value={dados.email} onChange={handleChange} />
      <input name="cidade" value={dados.cidade} onChange={handleChange} />
    </>
  );
}

Um único handler trata todos os inputs usando o atributo name. A computed property [name] atualiza o campo certo dinamicamente.

Controlled input
function Form() {
  const [nome, setNome] = useState("");

  return (
    <input
      value={nome}
      onChange={(e) => setNome(e.target.value)}
      placeholder="Nome"
    />
  );
}

Num input controlado, o valor vem do estado e cada tecla atualiza-o. O React é a única fonte de verdade.

Radio buttons
const [opcao, setOpcao] = useState("a");

<label>
  <input
    type="radio"
    value="a"
    checked={opcao === "a"}
    onChange={e => setOpcao(e.target.value)}
  />
  Opção A
</label>

Cada radio tem um value e fica checked quando corresponde ao estado. O onChange guarda o valor escolhido.

Validação em tempo real
const [email, setEmail] = useState("");
const [erro, setErro] = useState("");

const handleChange = (e) => {
  const valor = e.target.value;
  setEmail(valor);

  if (valor && !valor.includes("@")) {
    setErro("Email inválido");
  } else {
    setErro("");
  }
};

<input value={email} onChange={handleChange} />
{erro && <span className="erro">{erro}</span>}

A validação em tempo real verifica a cada tecla. Mostra o erro imediatamente e limpa-o quando o input fica válido.

Select e textarea controlados
function Form() {
  const [pais, setPais] = useState("pt");
  const [msg, setMsg] = useState("");

  return (
    <>
      <select value={pais} onChange={e => setPais(e.target.value)}>
        <option value="pt">Portugal</option>
        <option value="br">Brasil</option>
        <option value="ao">Angola</option>
      </select>

      <textarea
        value={msg}
        onChange={e => setMsg(e.target.value)}
        rows={4}
      />
    </>
  );
}

select e textarea também são controlados com value + onChange. No select, o value corresponde ao atributo value da option selecionada.

Formulário com submit
function FormLogin() {
  const [email, setEmail] = useState("");

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

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

Usa onSubmit no form e e.preventDefault() para evitar o recarregamento da página. Lê os valores do estado.

Eventos de teclado
<input
  onKeyDown={(e) => {
    if (e.key === "Enter") {
      pesquisar();
    }
  }}
/>

// e.key: a tecla premida
// e.code: o código físico da tecla

Os eventos onKeyDown/onKeyUp detetam teclas. Usa e.key para saber qual foi premida (ex.: "Enter").

Formulário com reset
const inicial = { nome: "", email: "" };
const [form, setForm] = useState(inicial);

const handleSubmit = async (e) => {
  e.preventDefault();
  await fetch("/api", {
    method: "POST",
    body: JSON.stringify(form),
  });
  setForm(inicial);  // limpar
};

<form onSubmit={handleSubmit}>
  <input value={form.nome}
    onChange={e => setForm({ ...form, nome: e.target.value })} />
  <button>Enviar</button>
</form>

Para limpar o formulário após o submit, basta repor o estado para o objeto inicial. Os inputs controlados atualizam automaticamente.

Actions: useActionState (React 19)
import { useActionState } from "react";
import { useFormStatus } from "react-dom";

async function criar(estadoAnterior, formData) {
  await fetch("/api/tarefas", {
    method: "POST",
    body: formData,
  });
  return { ok: true };
}

function Botao() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? "A gravar..." : "Gravar"}
    </button>
  );
}

function Form() {
  const [estado, action] = useActionState(criar, null);
  return (
    <form action={action}>
      <input name="titulo" />
      <Botao />
    </form>
  );
}

Com Actions (React 19), o form usa action={fn} e envia sem handlers manuais. O useActionState guarda o resultado e o useFormStatus expõe o estado pending — sem useState nem setLoading.

Passar argumentos
<button onClick={() => eliminar(item.id)}>
  Remover
</button>

<li onMouseEnter={() => setHover(id)}>
  {nome}
</li>

Para passar parâmetros ao handler, envolve-o numa arrow function. Assim controlas exatamente o que é enviado.

Debounce em pesquisa
const [texto, setTexto] = useState("");
const [resultados, setResultados] = useState([]);

useEffect(() => {
  const timer = setTimeout(() => {
    fetch(`/api?q=${texto}`)
      .then(r => r.json())
      .then(setResultados);
  }, 300);

  return () => clearTimeout(timer);
}, [texto]);

O debounce espera o utilizador parar de escrever antes de pesquisar. O cleanup cancela o timer anterior a cada tecla.

Submeter com Enter
function Chat() {
  const [msg, setMsg] = useState("");

  const enviar = () => {
    if (!msg.trim()) return;
    console.log("Enviado:", msg);
    setMsg("");
  };

  return (
    <input
      value={msg}
      onChange={e => setMsg(e.target.value)}
      onKeyDown={e => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();
          enviar();
        }
      }}
    />
  );
}

Use onKeyDown para detetar a tecla Enter. Verifique e.key === "Enter" e previna o default para não inserir nova linha.

Listas e Condicionais


15 cards
Renderizar lista
function ListaProdutos({ produtos }) {
  return (
    <ul>
      {produtos.map(p => (
        <li key={p.id}>{p.nome}</li>
      ))}
    </ul>
  );
}

Usa map para transformar cada item do array num elemento JSX. O React renderiza a lista completa.

Lista vazia
{itens.length === 0 ? (
  <p>Nenhum item encontrado.</p>
) : (
  <ul>
    {itens.map(i => <li key={i.id}>{i.nome}</li>)}
  </ul>
)}

Trata sempre o estado vazio com uma mensagem amiga, em vez de mostrar uma lista em branco.

Ordenação de lista
const [ordem, setOrdem] = useState("asc");

const ordenados = useMemo(() => {
  return [...itens].sort((a, b) =>
    ordem === "asc"
      ? a.nome.localeCompare(b.nome)
      : b.nome.localeCompare(a.nome)
  );
}, [itens, ordem]);

<button onClick={() =>
  setOrdem(o => o === "asc" ? "desc" : "asc")
}>
  Ordenar: {ordem}
</button>

Cria uma cópia com [...itens] antes de ordenar (o sort muta o original). O useMemo evita reordenar a cada render.

Infinite scroll
const [pagina, setPagina] = useState(1);
const [itens, setItens] = useState([]);

useEffect(() => {
  fetch(`/api/itens?page=${pagina}`)
    .then(r => r.json())
    .then(novos => setItens(prev => [...prev, ...novos]));
}, [pagina]);

// Detetar fim da página:
const onScroll = () => {
  if (window.innerHeight + window.scrollY >=
      document.body.offsetHeight - 200) {
    setPagina(p => p + 1);
  }
};
window.addEventListener("scroll", onScroll);

O infinite scroll carrega mais dados quando o utilizador chega ao fim. Deteta a posição do scroll e incrementa a página.

A importância do key
// ✅ Correto: id único e estável
{itens.map(i => <li key={i.id}>{i.nome}</li>)}

// ❌ Evitar: usar o índice
{itens.map((i, idx) => <li key={idx}>{i.nome}</li>)}

O key identifica cada item de forma única. Usar o índice pode causar bugs quando a lista é reordenada ou filtrada.

Variável de conteúdo
let conteudo;

if (loading) {
  conteudo = <Spinner />;
} else if (erro) {
  conteudo = <p>Erro ao carregar</p>;
} else {
  conteudo = <Dados dados={dados} />;
}

return <div>{conteudo}</div>;

Para condições com vários ramos, atribui o JSX a uma variável com if/else e renderiza-a no fim. Mais legível que ternários aninhados.

Paginação simples
const [pagina, setPagina] = useState(1);
const porPagina = 10;

const inicio = (pagina - 1) * porPagina;
const visiveis = itens.slice(inicio, inicio + porPagina);
const totalPaginas = Math.ceil(itens.length / porPagina);

return (
  <>
    {visiveis.map(i => <Cartao key={i.id} {...i} />)}
    <button disabled={pagina === 1}
      onClick={() => setPagina(p => p - 1)}>Anterior</button>
    <span>{pagina} / {totalPaginas}</span>
    <button disabled={pagina === totalPaginas}
      onClick={() => setPagina(p => p + 1)}>Seguinte</button>
  </>
);

A paginação mostra só uma fatia dos dados com slice. Os botões anterior/seguinte controlam a página atual.

Lista agrupada
const grupos = itens.reduce((acc, item) => {
  (acc[item.categoria] ??= []).push(item);
  return acc;
}, {});

return (
  <div>
    {Object.entries(grupos).map(([cat, lista]) => (
      <section key={cat}>
        <h3>{cat}</h3>
        <ul>
          {lista.map(i => <li key={i.id}>{i.nome}</li>)}
        </ul>
      </section>
    ))}
  </div>
);

Use reduce() para agrupar itens por categoria e depois Object.entries() para iterar os grupos com os seus itens.

Renderização condicional
// Ternário:
{logado ? <Painel /> : <Login />}

// && (curto-circuito):
{erros.length > 0 && (
  <div className="erro">{erros[0]}</div>
)}

O ternário escolhe entre duas opções. O operador && mostra algo só quando a condição é verdadeira.

Listas aninhadas
{categorias.map(cat => (
  <div key={cat.id}>
    <h3>{cat.nome}</h3>
    <ul>
      {cat.itens.map(item => (
        <li key={item.id}>{item.nome}</li>
      ))}
    </ul>
  </div>
))}

Para listas dentro de listas, faz map aninhado. Cada nível precisa do seu próprio key único.

Filtro em tempo real
const [busca, setBusca] = useState("");

const filtrados = useMemo(() => {
  if (!busca) return itens;
  const termo = busca.toLowerCase();
  return itens.filter(i =>
    i.nome.toLowerCase().includes(termo)
  );
}, [itens, busca]);

return (
  <>
    <input value={busca}
      onChange={e => setBusca(e.target.value)}
      placeholder="Pesquisar..." />
    {filtrados.map(i => <Cartao key={i.id} {...i} />)}
  </>
);

O filtro em tempo real usa useMemo para só recalcular quando a busca ou os itens mudam. A lista atualiza a cada tecla.

Condicionais com objeto
const icones = {
  sucesso: "✅",
  erro: "❌",
  aviso: "⚠️",
  info: "ℹ️",
};

function Alerta({ tipo, msg }) {
  return (
    <div className={`alerta-${tipo}`}>
      <span>{icones[tipo] ?? "❓"}</span>
      {msg}
    </div>
  );
}

// Uso:
<Alerta tipo="sucesso" msg="Guardado!" />

Um objeto de lookup substitui longas cadeias de if/else ou switch. O operador ?? dá um fallback para chaves inexistentes.

Filtrar e transformar
const ativos = produtos
  .filter(p => p.ativo)
  .map(p => (
    <Cartao key={p.id} produto={p} />
  ));

return <div>{ativos}</div>;

Combina filter (para selecionar) com map (para renderizar). Primeiro filtras, depois transformas em componentes.

Ternário vs &&
// Ternário: dois resultados possíveis
{admin ? <PainelAdmin /> : <PainelUser />}

// &&: mostra só se verdadeiro
{erros.length > 0 && <Alerta erros={erros} />}

// Cuidado: 0 && <X /> renderiza "0"!
{count > 0 && <p>{count} itens</p>}

Usa ternário quando há duas opções e && para mostrar ou esconder. Atenção: 0 && ... mostra o zero — verifica com > 0.

Virtualização (react-window)
import { FixedSizeList } from "react-window";

<FixedSizeList
  height={400}
  itemCount={10000}
  itemSize={35}
  width="100%"
>
  {({ index, style }) => (
    <div style={style}>Item {index}</div>
  )}
</FixedSizeList>

A virtualização renderiza só os itens visíveis no viewport. Com 10.000 itens, só ~20 estão no DOM. Essencial para listas enormes.

Estilos e CSS


10 cards
Inline styles
const estilo = {
  color: "white",
  backgroundColor: "blue",
  padding: "1rem",
  borderRadius: "8px"
};

<div style={estilo}>Olá</div>
<p style={{ fontSize: 14 }}>Texto</p>

O atributo style recebe um objeto JavaScript com propriedades em camelCase. Bom para estilos dinâmicos simples.

Variáveis CSS (tema)
/* tema.css */
:root {
  --cor-primaria: #3b82f6;
  --espaco: 1rem;
}

/* componente */
.cartao {
  color: var(--cor-primaria);
  padding: var(--espaco);
}

As variáveis CSS (--nome) centralizam cores e espaçamentos. Mudar o tema é tão simples como trocar o valor na raiz.

CSS-in-JS (styled-components)
import styled from "styled-components";

const Botao = styled.button`
  background: ${props => props.$primario ? "#0070f3" : "#eee"};
  color: ${props => props.$primario ? "#fff" : "#333"};
  padding: 8px 16px;
  border-radius: 6px;
  border: none;
`;

// Uso:
<Botao $primario>Salvar</Botao>
<Botao>Cancelar</Botao>

styled-components permite escrever CSS real dentro do JS. As props dinâmicas geram estilos condicionais sem classes.

CSS Modules
/* Botao.module.css */
.botao { padding: 8px 16px; }
.primario { background: blue; }
import styles from "./Botao.module.css";

<button className={styles.botao}>Clicar</button>

Os CSS Modules dão um nome único a cada classe, evitando conflitos globais. O estilo fica com escopo local ao componente.

Animações CSS
/* styles.css */
@keyframes aparecer {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.cartao {
  animation: aparecer 0.3s ease-out;
}
// No componente:
<div className="cartao">Conteúdo</div>

As animações CSS (@keyframes) dão transições suaves sem JavaScript. Aplica a classe ao elemento e o browser anima automaticamente.

Tailwind CSS
function Card({ titulo, ativo }) {
  return (
    <div className={
      "rounded-lg p-4 shadow " +
      (ativo ? "bg-blue-500 text-white" : "bg-white text-gray-800")
    }>
      <h2 className="text-xl font-bold">{titulo}</h2>
      <p className="mt-2 text-sm opacity-75">Conteúdo do card</p>
    </div>
  );
}

// Com template literals:
<div className={`p-4 ${ativo ? "bg-blue-500" : "bg-white"}`}>

Tailwind usa classes utilitárias diretamente no JSX. Combine com ternários ou template literals para estilos condicionais.

className condicional
<div className={`cartao ${ativo ? "ativo" : ""}`}>

<button className={`btn ${
  variante === "primary"
    ? "btn-primary"
    : "btn-secondary"
}`}>

Combina classes fixas e dinâmicas com template literals. A classe é aplicada conforme a condição.

clsx / classnames
import clsx from "clsx";

<button className={clsx(
  "btn",
  {
    "btn-primary": variante === "primary",
    "btn-lg": grande,
    "disabled": !ativo,
  }
)}>
  Clicar
</button>

A biblioteca clsx junta classes condicionalmente de forma limpa. Evita template literals complexos com múltiplas condições.

Estilos dinâmicos
function Barra({ progresso }) {
  return (
    <div
      style={{
        width: `${progresso}%`,
        backgroundColor: progresso > 80 ? "green" : "orange"
      }}
    />
  );
}

Calcula estilos a partir do estado ou props. O objeto de estilo é recriado a cada render com os valores atuais.

Dark mode toggle
const [tema, setTema] = useState("light");

useEffect(() => {
  document.documentElement.dataset.tema = tema;
}, [tema]);

<button onClick={() =>
  setTema(t => t === "light" ? "dark" : "light")
}>
  {tema === "light" ? "🌙" : "☀️"}
</button>

/* CSS: */
[data-tema="dark"] { --fundo: #1a1a2e; --texto: #eee; }
[data-tema="light"] { --fundo: #fff; --texto: #333; }

O dark mode alterna um atributo no html e o CSS usa variáveis para trocar as cores. O estado guarda a preferência.

Padrões Avançados


13 cards
Context API
import { createContext, useContext } from "react";

const ThemeCtx = createContext("light");

// Provider:
<ThemeCtx.Provider value="dark">
  <App />
</ThemeCtx.Provider>

// Consumer:
function Botao() {
  const tema = useContext(ThemeCtx);
  return <button className={tema}>...</button>;
}

O Context partilha dados com toda a árvore de componentes sem passar props por todos os níveis (prop drilling).

forwardRef
import { forwardRef } from "react";

const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

// No pai:
const ref = useRef();
<Input ref={ref} />
ref.current.focus();

O forwardRef permite passar uma ref para um elemento dentro de um componente filho. Útil para focar inputs ou medir elementos.

Error Boundary
import { Component } from "react";

class ErrorBoundary extends Component {
  state = { erro: null };

  static getDerivedStateFromError(erro) {
    return { erro };
  }

  render() {
    if (this.state.erro) {
      return <h1>Algo correu mal: {this.state.erro.message}</h1>;
    }
    return this.props.children;
  }
}

// Uso:
<ErrorBoundary>
  <ComponenteQuePodeFalhar />
</ErrorBoundary>

Um Error Boundary captura erros de renderização nos filhos e mostra um fallback. Ainda requer class component (único caso).

Dividir Context (performance)
// ❌ Um context gigante re-renderiza todos:
<Ctx.Provider value={{ user, tema, dispatch }}>

// ✅ Contexts separados re-renderizam só quem usa:
<UserCtx.Provider value={user}>
  <TemaCtx.Provider value={tema}>
    <App />
  </TemaCtx.Provider>
</UserCtx.Provider>

// Ou memoriza o value:
<Ctx.Provider value={useMemo(
  () => ({ user, dispatch }), [user]
)}>

Mudar um context re-renderiza todos os consumidores. Divide em contexts pequenos ou memoriza o value com useMemo para que cada componente só re-renderize quando os dados que usa mudarem.

Estado global simples
const AppCtx = createContext();

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(
    reducer, estadoInicial
  );
  return (
    <AppCtx.Provider value={{ state, dispatch }}>
      {children}
    </AppCtx.Provider>
  );
}

// Em qualquer componente:
const { state, dispatch } = useContext(AppCtx);

Junta useContext + useReducer para criar um estado global simples, sem bibliotecas externas.

Compound Components
function Tabs({ children }) {
  const [ativa, setAtiva] = useState(0);
  return (
    <div>
      {children.map((child, i) =>
        cloneElement(child, {
          ativa: i === ativa,
          onClick: () => setAtiva(i),
        })
      )}
    </div>
  );
}

<Tabs>
  <Tab label="Perfil">...</Tab>
  <Tab label="Definições">...</Tab>
</Tabs>

No padrão Compound Components, componentes pai e filho partilham estado implicitamente. A API fica declarativa e flexível.

Portal (createPortal)
import { createPortal } from "react-dom";

function Modal({ aberto, children, onFechar }) {
  if (!aberto) return null;

  return createPortal(
    <div className="overlay" onClick={onFechar}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body  // renderiza fora da árvore
  );
}

createPortal renderiza filhos num nó DOM diferente (ex.: body). Ideal para modais, tooltips e dropdowns que precisam escapar do overflow.

Lazy loading
import { lazy, Suspense } from "react";

const Painel = lazy(() => import("./Painel"));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Painel />
    </Suspense>
  );
}

O lazy carrega o componente só quando é preciso (code-splitting). O Suspense mostra um fallback durante o carregamento.

Slot pattern (children)
function Layout({ header, sidebar, children }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

<Layout
  header={<Nav />}
  sidebar={<Menu />}
>
  <Pagina />
</Layout>

O padrão slot usa props para injectar JSX em zonas específicas do layout. Mais explícito que children para múltiplas áreas.

Higher-Order Component (HOC)
function comAuth(Componente) {
  return function ComponenteProtegido(props) {
    const { user } = useAuth();

    if (!user) {
      return <p>Faça login para continuar.</p>;
    }

    return <Componente {...props} user={user} />;
  };
}

// Uso:
const PainelProtegido = comAuth(Painel);

// No router:
<PainelProtegido />

Um HOC é uma função que recebe um componente e devolve um novo com comportamento extra. Útil para autenticação, logging ou injeção de props.

Render props
function Dados({ url, children }) {
  const [dados, setDados] = useState(null);

  useEffect(() => {
    fetch(url).then(r => r.json()).then(setDados);
  }, [url]);

  return children(dados);
}

<Dados url="/api/user">
  {(user) => <p>{user?.nome}</p>}
</Dados>

No padrão render props, o children é uma função que recebe dados do componente. Inverte o controlo de forma flexível.

Suspense para dados
// Com React Query / SWR:
function Produtos() {
  const { data, isLoading } = useQuery(
    ["produtos"],
    () => fetch("/api/produtos").then(r => r.json())
  );

  if (isLoading) return <Spinner />;
  return <Lista itens={data} />;
}

// Ou com Suspense nativo (React 19+):
<Suspense fallback={<Spinner />}>
  <Produtos />
</Suspense>

Bibliotecas como React Query ou SWR gerem cache, loading e erros. O Suspense mostra o fallback enquanto os dados carregam.

ref como prop (React 19)
// React 19: ref é uma prop normal (sem forwardRef):
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// No pai:
const ref = useRef();
<Input ref={ref} />
ref.current.focus();

// Em React 18 era preciso:
// const Input = forwardRef((props, ref) => ...);

No React 19, a ref passa a ser uma prop normal dos componentes de função — o forwardRef deixa de ser necessário. Basta receber { ref } e aplicá-la ao elemento.

Ecossistema e CLI


16 cards
Criar projeto (Vite)
npm create vite@latest minha-app -- --template react
cd minha-app
npm install
npm run dev

O Vite cria um projeto React com hot reload instantâneo. É a forma recomendada de começar um projeto novo.

Estrutura de pastas
src/
├── components/    # Componentes reutilizáveis
├── pages/         # Páginas por rota
├── hooks/         # Custom hooks
├── services/      # Chamadas à API
├── utils/         # Funções utilitárias
└── App.jsx

Uma estrutura organizada por responsabilidade facilita encontrar e manter o código à medida que o projeto cresce.

Deploy (build)
# Build de produção
npm run build

# Preview local do build
npm run preview

# O output fica em dist/
# Faz deploy para: Vercel, Netlify, Cloudflare Pages

O build gera ficheiros otimizados (minificados, com hash) na pasta dist/. Qualquer servidor estático ou CDN os serve.

TanStack Router
import { createRouter, RouterProvider } from "@tanstack/react-router";

const router = createRouter({
  routes: [
    { path: "/", component: Home },
    { path: "/users/$id", component: UserDetail },
  ],
});

function App() {
  return <RouterProvider router={router} />;
}

// No componente:
const { id } = useParams({ from: "/users/$id" });

O TanStack Router é type-safe por natureza: rotas, params e search são inferidos automaticamente pelo TypeScript.

React Router
import { BrowserRouter, Routes, Route, Link }
  from "react-router-dom";

<BrowserRouter>
  <nav>
    <Link to="/">Início</Link>
    <Link to="/sobre">Sobre</Link>
  </nav>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/sobre" element={<Sobre />} />
    <Route path="/user/:id" element={<Perfil />} />
  </Routes>
</BrowserRouter>

O React Router gere a navegação numa SPA. Define rotas com Route e navega com Link sem recarregar a página.

Boas práticas
• Componentes pequenos e focados
• key estável em listas (não usar index)
• Estado o mais abaixo possível
• useMemo/useCallback só quando necessário
• Separar lógica em custom hooks
• TypeScript para props tipadas

Convenções que mantêm o código React limpo, rápido e fácil de manter a longo prazo.

Next.js (básico)
npx create-next-app@latest minha-app

// app/page.jsx (Server Component por defeito)
export default function Home() {
  return <h1>Olá Next.js</h1>;
}

// app/api/dados/route.js (API route)
export async function GET() {
  return Response.json({ ok: true });
}

// Routing por pastas: app/sobre/page.jsx = /sobre

O Next.js é um framework React com routing por ficheiros, SSR e API routes. Cada pasta em app/ é uma rota automaticamente.

Vitest (testes)
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import Contador from "./Contador";

test("mostra o contador", () => {
  render(<Contador />);
  expect(screen.getByText("0")).toBeInTheDocument();
});

test("incrementa ao clicar", async () => {
  render(<Contador />);
  await userEvent.click(screen.getByRole("button"));
  expect(screen.getByText("1")).toBeInTheDocument();
});

// Executar: npx vitest

Vitest é o test runner padrão do Vite. Com a Testing Library, teste o comportamento (clicar, ver texto) em vez da implementação.

useParams e useNavigate
import { useParams, useNavigate }
  from "react-router-dom";

function Perfil() {
  const { id } = useParams();
  const navigate = useNavigate();

  const voltar = () => navigate(-1);
  const irHome = () => navigate("/");
}

O useParams lê parâmetros da URL (:id). O useNavigate permite navegar programaticamente.

Variáveis de ambiente
// Ficheiro .env
VITE_API_URL=https://api.exemplo.com

// No código (Vite):
const url = import.meta.env.VITE_API_URL;

// Create React App:
const url = process.env.REACT_APP_API_URL;

As variáveis de ambiente guardam configurações (URLs, chaves públicas) fora do código. No Vite, começam por VITE_.

Zustand (estado global)
import { create } from "zustand";

const useStore = create((set) => ({
  count: 0,
  incrementar: () => set(s => ({ count: s.count + 1 })),
  reset: () => set({ count: 0 }),
}));

// Em qualquer componente:
function Contador() {
  const { count, incrementar } = useStore();
  return <button onClick={incrementar}>{count}</button>;
}

O Zustand é uma alternativa minimalista ao Redux para estado global. Sem providers, sem boilerplate — um hook simples.

Server Components
// Server Component (padrão no Next.js App Router):
// Corre no servidor e pode aceder à BD diretamente:
async function Produtos() {
  const produtos = await db.query(
    "SELECT * FROM produtos"
  );
  return (
    <ul>
      {produtos.map(p => <li key={p.id}>{p.nome}</li>)}
    </ul>
  );
}

// Client Component (precisa de hooks/eventos):
"use client";
function Contador() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

Server Components correm no servidor e enviam HTML pronto — ideais para buscar dados sem enviar JavaScript extra. Usa "use client" só nos componentes que precisam de hooks ou eventos.

Fetch com async/await
function Produtos() {
  const [dados, setDados] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function buscar() {
      const res = await fetch("/api/produtos");
      const json = await res.json();
      setDados(json);
      setLoading(false);
    }
    buscar();
  }, []);

  if (loading) return <p>A carregar...</p>;
  return <Lista itens={dados} />;
}

Padrão completo de data fetching: estado de loading, pedido assíncrono no useEffect e renderização condicional.

Axios (alternativa ao fetch)
import axios from "axios";

useEffect(() => {
  axios.get("/api/produtos")
    .then(res => setDados(res.data))
    .catch(err => setErro(err.message));
}, []);

// POST:
axios.post("/api/user", { nome: "Ana" });

O Axios é uma biblioteca de pedidos HTTP. Dá o JSON já tratado em res.data e tem uma API mais simples que o fetch.

React Hook Form
import { useForm } from "react-hook-form";

function Login() {
  const { register, handleSubmit, formState } = useForm();

  const onSubmit = (dados) => console.log(dados);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: true })} />
      {formState.errors.email && <span>Obrigatório</span>}
      <button>Entrar</button>
    </form>
  );
}

O React Hook Form gere formulários com performance (sem re-render a cada tecla). O register liga o input e a validação declarativa.

StrictMode
import { StrictMode } from "react";

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

// Em desenvolvimento, o StrictMode:
// - renderiza tudo 2x (apanha renders impuros)
// - executa efeitos 2x (apanha efeitos sem cleanup)
// Em produção não tem qualquer efeito.

O StrictMode é uma ferramenta de desenvolvimento que deteta problemas: renderiza tudo duas vezes e executa efeitos a mais, expondo renders impuros e efeitos sem cleanup. Não afeta a produção.