kb.erickguedes.com
React: Construindo Interfaces Modernas

Hooks Avançados

Aula 3 de 8

useCallback

Memoriza uma função para evitar recriação em toda renderização:

import { useState, useCallback, memo } from "react";

// Componente filho memoizado
const Botao = memo(function Botao({
  label,
  onClick,
}: {
  label: string;
  onClick: () => void;
}) {
  console.log(`Renderizando ${label}`);
  return <button onClick={onClick}>{label}</button>;
});

function Contador() {
  const [contador, setContador] = useState(0);

  // Sem useCallback: nova função a cada render → Botao re-renderiza
  // Com useCallback: mesma referência enquanto contador não mudar
  const incrementar = useCallback(() => {
    setContador((prev) => prev + 1);
  }, []);

  return (
    <div>
      <p>{contador}</p>
      <Botao label="Incrementar" onClick={incrementar} />
    </div>
  );
}

useMemo

Memoriza o resultado de uma computação cara:

import { useMemo, useState } from "react";

function ListaFiltrada({ items, filtro }: {
  items: string[];
  filtro: string;
}) {
  // Só recalcula se items ou filtro mudarem
  const filtrados = useMemo(() => {
    console.log("Filtrando...");
    return items.filter((item) =>
      item.toLowerCase().includes(filtro.toLowerCase())
    );
  }, [items, filtro]);

  return (
    <ul>
      {filtrados.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

Comparação useCallback vs useMemo

HookRetornaUso típico
useCallback(fn, deps)A própria funçãoPassar callbacks para componentes memoizados
useMemo(() => valor, deps)O valor computadoComputações caras (filtros, ordenações, transformações)

useReducer

Para estados com lógica complexa (alternativa ao useState):

import { useReducer } from "react";

interface State {
  contador: number;
  ultimaAcao: string;
}

type Action =
  | { type: "incrementar"; valor: number }
  | { type: "decrementar"; valor: number }
  | { type: "resetar" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "incrementar":
      return {
        contador: state.contador + action.valor,
        ultimaAcao: `incrementou ${action.valor}`,
      };
    case "decrementar":
      return {
        contador: state.contador - action.valor,
        ultimaAcao: `decrementou ${action.valor}`,
      };
    case "resetar":
      return { contador: 0, ultimaAcao: "resetou" };
    default:
      return state;
  }
}

function Contador() {
  const [state, dispatch] = useReducer(reducer, {
    contador: 0,
    ultimaAcao: "iniciou",
  });

  return (
    <div>
      <p>Contador: {state.contador}</p>
      <p>Última ação: {state.ultimaAcao}</p>
      <button onClick={() => dispatch({ type: "incrementar", valor: 1 })}>
        +1
      </button>
      <button onClick={() => dispatch({ type: "decrementar", valor: 1 })}>
        -1
      </button>
      <button onClick={() => dispatch({ type: "resetar" })}>
        Reset
      </button>
    </div>
  );
}

Immer + useReducer

npm install immer
import { useReducer } from "react";
import { produce } from "immer";

interface Item {
  id: number;
  texto: string;
  concluido: boolean;
}

type Action =
  | { type: "adicionar"; texto: string }
  | { type: "toggle"; id: number }
  | { type: "remover"; id: number };

function reducer(draft: Item[], action: Action) {
  switch (action.type) {
    case "adicionar":
      draft.push({ id: Date.now(), texto: action.texto, concluido: false });
      break;
    case "toggle": {
      const item = draft.find((t) => t.id === action.id);
      if (item) item.concluido = !item.concluido;
      break;
    }
    case "remover":
      return draft.filter((t) => t.id !== action.id);
  }
}

function ListaTarefas() {
  const [items, dispatch] = useReducer(produce(reducer), []);

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <span
            onClick={() => dispatch({ type: "toggle", id: item.id })}
            style={{
              textDecoration: item.concluido ? "line-through" : "none",
            }}
          >
            {item.texto}
          </span>
          <button onClick={() => dispatch({ type: "remover", id: item.id })}>
            ✕
          </button>
        </li>
      ))}
    </ul>
  );
}

useContext

Compartilha estado entre componentes sem prop drilling:

import { createContext, useContext, useState, type ReactNode } from "react";

// 1. Criar contexto
interface TemaContextType {
  tema: "claro" | "escuro";
  toggleTema: () => void;
}

const TemaContext = createContext<TemaContextType | null>(null);

// 2. Provider
function TemaProvider({ children }: { children: ReactNode }) {
  const [tema, setTema] = useState<"claro" | "escuro">("claro");

  const toggleTema = () =>
    setTema((prev) => (prev === "claro" ? "escuro" : "claro"));

  return (
    <TemaContext.Provider value={{ tema, toggleTema }}>
      {children}
    </TemaContext.Provider>
  );
}

// 3. Hook customizado
function useTema() {
  const context = useContext(TemaContext);
  if (!context) throw new Error("useTema deve estar dentro de TemaProvider");
  return context;
}

// 4. Consumir
function BotaoTema() {
  const { tema, toggleTema } = useTema();
  return (
    <button onClick={toggleTema}>
      Tema atual: {tema}
    </button>
  );
}

// 5. App
function App() {
  return (
    <TemaProvider>
      <BotaoTema />
    </TemaProvider>
  );
}

Custom Hooks

Regras: começar com use, chamar hooks apenas no nível superior, apenas em componentes React ou custom hooks.

import { useState, useEffect } from "react";

// Hook: useLocalStorage
function useLocalStorage<T>(chave: string, valorInicial: T) {
  const [valor, setValor] = useState<T>(() => {
    const stored = localStorage.getItem(chave);
    return stored ? (JSON.parse(stored) as T) : valorInicial;
  });

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

  return [valor, setValor] as const;
}

// Hook: useFetch
function useFetch<T>(url: string) {
  const [dados, setDados] = useState<T | null>(null);
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState<Error | null>(null);

  useEffect(() => {
    let cancelado = false;

    async function fetchData() {
      try {
        setCarregando(true);
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        if (!cancelado) setDados(json);
      } catch (err) {
        if (!cancelado) setErro(err as Error);
      } finally {
        if (!cancelado) setCarregando(false);
      }
    }

    fetchData();
    return () => { cancelado = true; };
  }, [url]);

  return { dados, carregando, erro };
}

// Uso
function Perfil({ usuarioId }: { usuarioId: number }) {
  const { dados, carregando, erro } = useFetch<{ nome: string }>(
    `/api/usuarios/${usuarioId}`
  );

  if (carregando) return <p>Carregando...</p>;
  if (erro) return <p>Erro: {erro.message}</p>;

  return <h1>{dados?.nome}</h1>;
}

useDebugValue

function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handler);
    return () => window.removeEventListener("mousemove", handler);
  }, []);

  useDebugValue(pos, (p) => `(${p.x}, ${p.y})`);
  return pos;
}

useId

Gera IDs únicos estáveis para acessibilidade:

import { useId } from "react";

function InputComLabel({ label }: { label: string }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}

Lab: Hook de Paginação

function usePaginacao<T>(items: T[], itemsPorPagina: number) {
  const [pagina, setPagina] = useState(0);
  const totalPaginas = Math.ceil(items.length / itemsPorPagina);

  const paginaItems = useMemo(
    () => items.slice(pagina * itemsPorPagina, (pagina + 1) * itemsPorPagina),
    [items, pagina, itemsPorPagina]
  );

  const avancar = useCallback(
    () => setPagina((p) => Math.min(p + 1, totalPaginas - 1)),
    [totalPaginas]
  );

  const voltar = useCallback(
    () => setPagina((p) => Math.max(p - 1, 0)),
    []
  );

  return {
    pagina,
    paginaItems,
    totalPaginas,
    avancar,
    voltar,
    setPagina,
  };
}

Custom hooks são a forma mais elegante de extrair lógica de estado e efeitos colaterais. Se você copia hooks entre componentes, extraia um custom hook.