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
| Hook | Retorna | Uso típico |
|---|---|---|
useCallback(fn, deps) | A própria função | Passar callbacks para componentes memoizados |
useMemo(() => valor, deps) | O valor computado | Computaçõ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.