Introdução ao Problema de Performance em React
Quando construímos aplicações web modernas, uma das maiores reclamações dos usuários não é sobre funcionalidade, mas sobre responsividade. Você já clicou em um botão e sentiu aquele lag frustrante? Aquele atraso entre a ação e a reação visual? Isso acontece porque o React precisa processar a atualização de estado, renderizar novos componentes e atualizar o DOM — tudo isso acontece no mesmo thread que executa JavaScript, CSS e responde a eventos do usuário.
O React 18 introduziu dois hooks revolucionários para resolver esse problema de forma elegante: useTransition e useOptimistic. Eles permitem que você marque atualizações como "não urgentes", liberando o thread principal para responder a interações críticas como cliques e digitação. Neste artigo, você aprenderá não apenas como usá-los, mas por que funcionam e quando realmente faz diferença.
useTransition: Priorização de Atualizações de Estado
O Conceito Fundamental
useTransition permite que você marque uma atualização de estado como uma transição — uma operação que pode ser interrompida e resumida sem prejudicar a experiência do usuário. Ao invés de travar o navegador enquanto processa algo pesado, React pausa o trabalho, processa eventos do usuário que chegam, e depois continua.
O hook retorna dois valores: uma função startTransition que você chama com código que atualiza estado, e um booleano isPending que indica se a transição está em progresso. Isso permite que você mostre feedback visual enquanto o React trabalha nos bastidores.
Exemplo Prático: Filtro de Lista Pesada
Imagine uma aplicação que filtra uma lista de 10 mil itens enquanto o usuário digita. Sem useTransition, a interface congela. Com ele, a digitação permanece responsiva:
import { useState, useTransition } from 'react';
export function FilterableList() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`
}));
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
const handleInputChange = (e) => {
const value = e.target.value;
// Atualização urgente: o input responde imediatamente
setQuery(value);
// Atualização não urgente: filtragem acontece em background
startTransition(() => {
// Aqui você poderia atualizar outro state
// que depende da filtragem pesada
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Digite para filtrar..."
/>
{isPending && <p>Filtrando...</p>}
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
Observe que setQuery(value) acontece fora de startTransition. Isso é intencional — queremos que o input responda imediatamente ao usuário. Se você quiser, pode colocar a filtragem em um estado separado dentro de startTransition, mas neste caso simples, o React otimiza automaticamente.
Padrão Avançado: Múltiplos Estados com Transição
Quando você tem lógica mais complexa, vale a pena separar claramente qual estado é urgente e qual não:
import { useState, useTransition } from 'react';
export function SearchWithResults() {
const [inputValue, setInputValue] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
// Urgente: atualiza o input imediatamente
setInputValue(value);
// Não urgente: calcula e renderiza resultados em background
startTransition(() => {
// Simulando busca pesada (em produção, seria uma API call)
const filtered = simulateExpensiveSearch(value);
setResults(filtered);
});
};
return (
<div>
<input
value={inputValue}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Buscar..."
/>
{isPending && <div className="spinner">Carregando resultados...</div>}
<ul className={isPending ? 'opacity-50' : ''}>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
function simulateExpensiveSearch(query) {
// Simulação de operação pesada
const start = performance.now();
while (performance.now() - start < 500) {}
return Array.from({ length: 50 }, (_, i) => ({
id: i,
title: `Resultado: ${query} - ${i}`
}));
}
useOptimistic: Feedback Imediato com Sincronização Segura
Quando Você Precisa Adivinhar o Futuro
useOptimistic resolve um problema diferente: quando você envia uma ação para o servidor (POST, PUT, DELETE), o usuário quer ver o resultado imediatamente, mas você só terá a confirmação em alguns milissegundos ou segundos. Se você esperar a resposta do servidor, a interface parece lenta. Se você atualizar o estado antes da confirmação e a requisição falhar, fica confuso.
A solução é atualizar o estado otimisticamente — mostrar o resultado esperado enquanto a requisição está em progresso, e fazer rollback se falhar. useOptimistic torna isso seguro e simples.
Exemplo Prático: Like Button
Vamos implementar um botão de like que atualiza a contagem imediatamente, mesmo esperando a resposta do servidor:
import { useOptimistic, useState } from 'react';
export function PostWithLike() {
const [post, setPost] = useState({
id: 1,
title: 'Meu Primeiro Post',
likes: 42,
liked: false
});
const [optimisticPost, addOptimisticLike] = useOptimistic(
post,
(currentPost, newLikes) => ({
...currentPost,
likes: newLikes,
liked: !currentPost.liked
})
);
const handleLike = async () => {
// Atualiza otimisticamente
addOptimisticLike(optimisticPost.liked ? post.likes - 1 : post.likes + 1);
try {
// Envia para o servidor
const response = await fetch(`/api/posts/${post.id}/like`, {
method: 'POST'
});
const updatedPost = await response.json();
// Sincroniza com a resposta real do servidor
setPost(updatedPost);
} catch (error) {
// Se falhar, o estado otimístico é descartado automaticamente
// e volta ao valor original
console.error('Erro ao fazer like:', error);
}
};
return (
<div className="post">
<h2>{optimisticPost.title}</h2>
<button
onClick={handleLike}
className={optimisticPost.liked ? 'liked' : ''}
>
❤️ {optimisticPost.likes}
</button>
</div>
);
}
Veja como funciona:
- Você clica no botão
- O estado muda imediatamente — o contador aumenta e a cor muda
- Em paralelo, a requisição HTTP é enviada
- Se a resposta vier com sucesso,
setPostsincroniza o estado real - Se falhar, o React automaticamente reverte para o estado anterior sem você fazer nada
Padrão Avançado: Múltiplas Ações Otimistas
Em aplicações reais, você pode ter múltiplas ações pendentes. useOptimistic permite atualizações em fila:
import { useOptimistic, useState } from 'react';
export function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, title: 'Aprender React 18', completed: false }
]);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
const handleAddTodo = async (title) => {
const tempTodo = {
id: Date.now(),
title,
completed: false
};
// Mostra o novo todo imediatamente
addOptimisticTodo(tempTodo);
try {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
});
const createdTodo = await response.json();
// Substitui o otimista pelo real (com ID real do servidor)
setTodos(current =>
current.map(t => t.id === tempTodo.id ? createdTodo : t)
);
} catch (error) {
console.error('Erro ao criar todo:', error);
// Automaticamente reverte
}
};
return (
<div>
<button onClick={() => handleAddTodo('Novo Todo')}>
Adicionar
</button>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
Combinando Ambos os Hooks: O Padrão Completo
Quando Usar Cada Um
useTransition é para atualizações de estado derivadas — quando mudar um estado causa cálculos pesados. useOptimistic é para interações com servidor — quando você quer mostrar resultado antes da confirmação. Frequentemente, você usa ambos na mesma aplicação, em contextos diferentes.
Mas existe um caso de uso poderoso onde eles trabalham juntos: formulários com validação assíncrona pesada. Você mostra feedback imediato (otimista) enquanto valida no servidor em background (transição).
Exemplo Prático: Formulário de Reserva
import { useState, useTransition, useOptimistic } from 'react';
export function BookingForm() {
const [booking, setBooking] = useState({
date: '',
time: '',
guests: 1,
status: 'idle' // idle, submitting, success, error
});
const [optimisticBooking, addOptimisticBooking] = useOptimistic(
booking,
(current, updates) => ({ ...current, ...updates })
);
const [isPending, startTransition] = useTransition();
const handleSubmit = async (e) => {
e.preventDefault();
// Atualiza otimisticamente com status de submissão
addOptimisticBooking({ status: 'submitting' });
// Processamento em background (pode ser pesado)
startTransition(async () => {
try {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(booking)
});
if (response.ok) {
const result = await response.json();
setBooking(prev => ({
...prev,
status: 'success',
...result
}));
} else {
setBooking(prev => ({
...prev,
status: 'error'
}));
}
} catch (error) {
setBooking(prev => ({
...prev,
status: 'error'
}));
}
});
};
const handleChange = (e) => {
const { name, value } = e.target;
setBooking(prev => ({
...prev,
[name]: value
}));
};
return (
<form onSubmit={handleSubmit}>
<input
type="date"
name="date"
value={optimisticBooking.date}
onChange={handleChange}
disabled={optimisticBooking.status === 'submitting'}
/>
<input
type="time"
name="time"
value={optimisticBooking.time}
onChange={handleChange}
disabled={optimisticBooking.status === 'submitting'}
/>
<input
type="number"
name="guests"
min="1"
max="10"
value={optimisticBooking.guests}
onChange={handleChange}
disabled={optimisticBooking.status === 'submitting'}
/>
<button type="submit" disabled={optimisticBooking.status === 'submitting'}>
{optimisticBooking.status === 'submitting' ? 'Reservando...' : 'Reservar'}
</button>
{optimisticBooking.status === 'success' && (
<p className="success">Reserva confirmada!</p>
)}
{optimisticBooking.status === 'error' && (
<p className="error">Erro ao fazer reserva. Tente novamente.</p>
)}
{isPending && (
<p className="info">Processando reserva...</p>
)}
</form>
);
}
Neste exemplo:
- O usuário submete o formulário
- Imediatamente, o estado é atualizado otimisticamente com
status: 'submitting' - O botão desabilita e mostra "Reservando..."
- A requisição HTTP é enviada dentro de
startTransition - Se suceder,
setBookingatualiza com a resposta real - Se falhar, o estado reverte e mostra erro
Otimizações Práticas e Armadilhas Comuns
O Perigo do Uso Incorreto
Um erro comum é colocar toda a lógica dentro de startTransition. Lembre-se: queremos apenas adiar o que é não-urgente. Se você colocar setInput dentro de startTransition, o input lag volta.
// ❌ ERRADO: input fica lento
const handleChange = (e) => {
startTransition(() => {
setInputValue(e.target.value); // Deve estar fora!
});
};
// ✅ CORRETO: input rápido, cálculos em background
const handleChange = (e) => {
setInputValue(e.target.value); // Urgente
startTransition(() => {
setExpensiveResult(calculateSomething(e.target.value)); // Não urgente
});
};
Medindo o Impacto Real
Para validar se você realmente precisa dessas otimizações, use as DevTools do React ou o Performance API nativo:
export function PerformanceMonitor({ children }) {
return (
<>
<React.Profiler
id="main"
onRender={(id, phase, actualDuration) => {
if (actualDuration > 16) { // ~60fps threshold
console.warn(
`Render lento em ${id}: ${actualDuration.toFixed(2)}ms`
);
}
}}
>
{children}
</React.Profiler>
</>
);
}
Compatibilidade com Server Components (React 19)
Se você estiver usando React 19 com Server Components, useOptimistic funciona em Client Components e pode trabalhar com Server Actions:
'use client';
import { useOptimistic } from 'react';
export function ClientComponent({ updateServerData }) {
const [optimisticData, addOptimisticUpdate] = useOptimistic(
null,
(_, newValue) => newValue
);
const handleClick = async () => {
addOptimisticUpdate('Loading...');
await updateServerData();
};
return (
<button onClick={handleClick}>
{optimisticData || 'Click'}
</button>
);
}
Conclusão
Aprendemos que useTransition e useOptimistic são ferramentas fundamentais para construir aplicações React modernas com excelente experiência do usuário. useTransition resolve o problema de renderizações pesadas priorizando atualizações urgentes, enquanto useOptimistic resolve o problema de latência de rede mostrando feedback imediato sem sacrificar a segurança dos dados.
O aprendizado prático mais importante é simplicidade: não comece a usar esses hooks por usar. Meça se há realmente problemas de performance, identifique a causa (renderização ou requisição), e aplique a solução apropriada. Um useTransition bem colocado pode transformar uma interface que parecia ruim em excelente, com apenas 3 linhas de código.