Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { memo, useMemo, useCallback, useTransition } from "react";
// Memoiza componente caro
const ExpensiveList = memo(function ExpensiveList({
items,
onSelect,
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// Pai: estabiliza props passadas para filhos memoizados
function Parent() {
const [items] = useState<Item[]>(initialItems);
const [selected, setSelected] = useState<string | null>(null);
// Referência de callback estável
const handleSelect = useCallback((id: string) => {
setSelected(id);
}, []);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}Quando usar isso: Apenas depois que o profiling revelar um problema de performance. Otimização prematura adiciona complexidade sem ganho. Use o React DevTools Profiler para medir primeiro, depois aplique memo, useMemo ou useCallback onde eles realmente ajudam.
import {
memo,
useMemo,
useCallback,
useState,
useTransition,
useDeferredValue,
} from "react";
// --- Lista virtualizada com linhas memoizadas ---
interface Contact {
id: string;
name: string;
email: string;
avatar: string;
}
const ContactRow = memo(function ContactRow({
contact,
isSelected,
onSelect,
}: {
contact: Contact;
isSelected: boolean;
onSelect: (id: string) => void;
}) {
return (
<div
onClick={() => onSelect(contact.id)}
className={`flex items-center gap-3 p-3 cursor-pointer ${
isSelected ? "bg-blue-50 border-l-4 border-blue-500" : "hover:bg-gray-50"
}`}
>
<img
src={contact.avatar}
alt=""
className="w-10 h-10 rounded-full"
loading="lazy"
/>
<div>
<p className="font-medium">{contact.name}</p>
<p className="text-sm text-gray-500">{contact.email}</p>
</div>
</div>
);
});
// --- Pesquisa com valor diferido ---
function ContactList({ contacts }: { contacts: Contact[] }) {
const [query, setQuery] = useState("");
const [selectedId, setSelectedId] = useState<string | null>(null);
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// Filtro caro só reexecuta quando deferredQuery ou contacts mudam
const filtered = useMemo(
() =>
contacts.filter(
(c) =>
c.name.toLowerCase().includes(deferredQuery.toLowerCase()) ||
c.email.toLowerCase().includes(deferredQuery.toLowerCase())
),
[contacts, deferredQuery]
);
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
return (
<div className="max-w-md border rounded-lg overflow-hidden">
<div className="p-3 border-b">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pesquisar contatos..."
className="w-full px-3 py-2 border rounded"
/>
</div>
<div
className={`max-h-96 overflow-y-auto ${isStale ? "opacity-60" : ""}`}
>
{filtered.length === 0 ? (
<p className="p-4 text-gray-500 text-center">Nenhum contato encontrado</p>
) : (
filtered.map((contact) => (
<ContactRow
key={contact.id}
contact={contact}
isSelected={contact.id === selectedId}
onSelect={handleSelect}
/>
))
)}
</div>
<div className="p-2 border-t text-sm text-gray-400 text-center">
{filtered.length} de {contacts.length} contatos
</div>
</div>
);
}
// --- Transição para troca de abas ---
function TabPanel({ tabs }: { tabs: { id: string; label: string; content: () => JSX.Element }[] }) {
const [activeTab, setActiveTab] = useState(tabs[0].id);
const [isPending, startTransition] = useTransition();
const handleTabChange = (tabId: string) => {
startTransition(() => {
setActiveTab(tabId);
});
};
const ActiveContent = tabs.find((t) => t.id === activeTab)?.content;
return (
<div>
<div className="flex border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={`px-4 py-2 -mb-px ${
activeTab === tab.id
? "border-b-2 border-blue-500 font-medium"
: "text-gray-500"
}`}
>
{tab.label}
</button>
))}
</div>
<div className={`p-4 ${isPending ? "opacity-50 transition-opacity" : ""}`}>
{ActiveContent && <ActiveContent />}
</div>
</div>
);
}O que isso demonstra:
memo em ContactRow pula re-renderizações quando as props não mudaramuseCallback estabiliza handleSelect para que não quebre memouseDeferredValue mantém a entrada de pesquisa responsiva enquanto a filtragem é adiadauseMemo armazena em cache a lista filtrada para evitar recomputação a cada pressionamento de teclauseTransition marca a troca de abas como não urgente, mantendo as interações suavesReact.memo(Component) envolve um componente para que ele só re-renderize quando suas props mudarem (comparação superficial por padrão).useMemo(fn, deps) armazena em cache um valor computado e só recomputa quando as dependências mudam.useCallback(fn, deps) armazena em cache uma referência de função - equivalente a useMemo(() => fn, deps).useDeferredValue(value) retorna uma cópia diferida que atrasa durante atualizações urgentes, permitindo que a entrada permaneça responsiva.useTransition envolve atualizações de estado como não urgentes, permitindo que o React interrompa a renderização se uma atualização mais urgente chegar.memo/useMemo/useCallback manuais desnecessários.| API | Entrada | Saída | Comportamento de Re-renderização |
|---|---|---|---|
memo(Component, areEqual?) | Componente, comparador opcional | Componente memoizado | Pula a renderização se as props forem superficialmente iguais |
useMemo(fn, deps) | Função de fábrica, array de dependências | Valor em cache do tipo T | Recomputa apenas quando os deps mudam |
useCallback(fn, deps) | Função, array de dependências | Referência de função estável | Nova referência apenas quando os deps mudam |
useDeferredValue(value) | Qualquer valor | Cópia diferida do valor | Atualiza com menor prioridade |
useTransition() | Nenhum | [isPending, startTransition] | Marca atualizações como interruptíveis |
Comparação personalizada para memo:
const Chart = memo(
function Chart({ data, config }: ChartProps) {
// Renderização cara
return <canvas />;
},
(prev, next) => {
// Re-renderiza apenas se o comprimento dos dados ou a configuração mudarem
return (
prev.data.length === next.data.length &&
prev.config.type === next.config.type
);
}
);Profiling com React DevTools:
import { Profiler, type ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.table({ id, phase, actualDuration, baseDuration });
};
function App() {
return (
<Profiler id="ContactList" onRender={onRender}>
<ContactList contacts={contacts} />
</Profiler>
);
}Evitando re-renderizações com o padrão children:
// Isso faz com que SearchResults re-renderize quando count muda:
function BadParent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<SearchResults /> {/* Re-renderiza desnecessariamente */}
</div>
);
}
// Correção: eleve o estado para um componente menor
function GoodParent() {
return (
<div>
<Counter /> {/* estado count escopado aqui */}
<SearchResults /> {/* Não re-renderiza mais */}
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}memo preserva os tipos de props do componente. Componentes genéricos exigem um wrapper: memo(Component) as typeof Component.useMemo<T> e useCallback inferem tipos da função de fábrica. Anotações de tipo explícitas raramente são necessárias.areEqual em memo recebe (prevProps, nextProps) e deve retornar true para pular a re-renderização.Otimização prematura - Adicionar memo, useMemo e useCallback em todos os lugares adiciona complexidade e pode realmente desacelerar componentes rápidos devido à sobrecarga de comparação. Correção: Faça o profiling primeiro com o React DevTools. Otimize apenas componentes que mostram custo de re-renderização mensurável.
Referências de props instáveis quebrando memo - Passar onClick={() => {}} ou style={{ color: "red" }} inline cria novas referências a cada renderização, invalidando memo. Correção: Use useCallback para funções e useMemo para objetos/arrays passados para filhos memoizados.
Dependência ausente em useMemo/useCallback - Omitir uma dependência leva a closures obsoletos. Correção: Inclua todos os valores referenciados no array de dependências. Use a regra ESLint react-hooks/exhaustive-deps.
Memoizar computações baratas - useMemo para uma simples concatenação de strings custa mais do que simplesmente recomputá-la. Correção: Memoize apenas computações que são realmente caras (filtrar grandes arrays, cálculos complexos, criar objetos para contexto).
Prop key reiniciando memo - Mudar a key de um componente faz com que o React o desmonte e remonte, independentemente de memo. Correção: Use chaves estáveis (IDs, não índices de array) a menos que você queira intencionalmente um reset.
Grandes árvores de componentes sem virtualização - Memoizar 10.000 itens de lista é mais lento do que virtualizar para renderizar apenas os itens visíveis. Correção: Use @tanstack/react-virtual, react-window ou CSS content-visibility: auto para listas grandes.
| Abordagem | Trade-off |
|---|---|
React.memo + useCallback | Manual, mas preciso; boilerplate |
| Compilador React (experimental) | Memoização automática; experimental, não pronto para produção |
| Virtualização | Renderiza apenas itens visíveis; comportamento de rolagem mais complexo |
| Colocação de estado | Mova o estado para mais perto de onde ele é usado; a correção mais simples |
useDeferredValue | Mantém a UI responsiva; mostra dados obsoletos brevemente |
CSS content-visibility | Renderização preguiçosa nativa do navegador; nenhuma API React necessária |
| Web Workers | Descarrega computação; transferência de dados complexa |
useState ou useReducer.memo).const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filtered = useMemo(
() => contacts.filter((c) => c.name.includes(deferredQuery)),
[contacts, deferredQuery]
);query, mas deferredQuery atrasa.isStale pode ser usado para mostrar um indicador visual (por exemplo, opacidade reduzida) enquanto a filtragem alcança.useTransition envolve uma atualização de estado para marcá-la como não urgente, mantendo a UI atual visível.useDeferredValue cria uma cópia diferida de um valor que se atualiza com menor prioridade.useTransition quando você controla a atualização do estado. Use useDeferredValue quando você recebe um valor de props.onClick={() => {}} ou style={{ color: "red" }} inline criam uma nova referência a cada renderização.React.memo compara props por referência, então ele vê uma "mudança" e re-renderiza.useCallback para funções e useMemo para objetos/arrays passados para filhos memoizados.useMemo tem sobrecarga: ele armazena o valor anterior, compara dependências e gerencia memória.// Ruim: SearchResults re-renderiza quando count muda
function BadParent() {
const [count, setCount] = useState(0);
return <div><button onClick={() => setCount(c => c + 1)}>{count}</button><SearchResults /></div>;
}
// Bom: mova o estado para um componente menor
function GoodParent() {
return <div><Counter /><SearchResults /></div>;
}SearchResults não re-renderiza mais porque seu pai (GoodParent) não possui o estado em mudança.memo(Component) pode perder parâmetros de tipo genérico.memo(Component) as typeof Component.const Chart = memo(
function Chart({ data, config }: ChartProps) { return <canvas />; },
(prev, next) => prev.data.length === next.data.length && prev.config.type === next.config.type
);(prevProps, nextProps) e retorna true para pular a re-renderização, false para re-renderizar.@tanstack/react-virtual e react-window renderizam apenas os itens no viewport.content-visibility: auto fornece renderização preguiçosa nativa do navegador sem uma biblioteca React.memo, useMemo e useCallback manuais desnecessários.useTransition e useDeferredValue para renderização assíncronachildren-como-props evita re-renderizações do paiRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥