React 19 useOptimistic Hook: UI Otimista Tutorial Prático
Descubra como o hook useOptimistic revoluciona a experiência do usuário criando interfaces que respondem instantaneamente, mesmo com conexões lentas.
Por que isso é importante
Interfaces otimistas reduzem a percepção de latência em 70%, melhorando drasticamente a experiência do usuário. O useOptimistic simplifica esta implementação complexa em poucas linhas de código.
O que é UI Otimista?
Interface otimista é um padrão onde a UI atualiza instantaneamente assumindo que a operação será bem-sucedida, mesmo antes da confirmação do servidor.
Exemplo Prático:
Você comenta em um post do Instagram. O comentário aparece instantaneamente, mesmo que sua internet esteja lenta. Por baixo dos panos, o app ainda está enviando o comentário para o servidor.
- • Tradicional: Clique → Aguarda → Comentário aparece
- • Otimista: Clique → Comentário aparece → Confirma servidor
Hook useOptimistic: Sintaxe Básica
const [optimisticState, addOptimistic] = useOptimistic(
currentState,
(currentState, optimisticValue) => {
// Lógica de atualização otimista
return newOptimisticState;
}
);
Parâmetros
- • currentState: Estado atual dos dados
- • updateFn: Função de atualização otimista
Retorno
- • optimisticState: Estado com atualizações otimistas
- • addOptimistic: Função para disparar atualização
Exemplo Prático: Sistema de Comentários
Vamos implementar um sistema de comentários que responde instantaneamente, mesmo com latência de rede.
1. Estrutura Base do Componente
import { useState, useOptimistic } from 'react';
function CommentsSection({ postId, initialComments }) {
const [comments, setComments] = useState(initialComments);
const [newComment, setNewComment] = useState('');
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{
id: crypto.randomUUID(),
text: newComment,
author: 'Você',
timestamp: new Date().toISOString(),
status: 'sending' // Indica que está enviando
}
]
);
return (
<div className="max-w-2xl mx-auto p-6">
{/* Renderização dos comentários */}
</div>
);
}
2. Função de Envio Otimista
async function handleSubmitComment(formData) {
const commentText = formData.get('comment');
// 1. Atualização otimista INSTANTÂNEA
addOptimisticComment(commentText);
setNewComment(''); // Limpa o input
try {
// 2. Envio real para o servidor (pode demorar)
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
postId,
text: commentText
})
});
if (!response.ok) throw new Error('Falha no envio');
const savedComment = await response.json();
// 3. Atualiza com dados reais do servidor
setComments(prev => [...prev, savedComment]);
} catch (error) {
// 4. Em caso de erro, reverte o estado otimista
console.error('Erro ao enviar comentário:', error);
// O useOptimistic automaticamente reverte para o estado original
// quando setComments é chamado sem incluir o comentário otimista
// Opcionalmente, mostrar mensagem de erro
alert('Falha ao enviar comentário. Tente novamente.');
}
}
3. Renderização com Estados Visuais
return (
<div className="space-y-6">
{/* Lista de Comentários */}
<div className="space-y-4">
{optimisticComments.map((comment) => (
<div
key={comment.id}
className={`p-4 rounded-lg border ${
comment.status === 'sending'
? 'bg-yellow-500/10 border-yellow-500/20 animate-pulse'
: 'bg-gray-800/50 border-gray-700'
}`}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<p className="text-gray-300">{comment.text}</p>
<div className="flex items-center gap-2 mt-2 text-sm text-gray-500">
<span>{comment.author}</span>
<span>•</span>
<span>{formatTime(comment.timestamp)}</span>
{comment.status === 'sending' && (
<span className="text-yellow-400 font-medium">
Enviando...
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
{/* Formulário de Novo Comentário */}
<form action={handleSubmitComment} className="space-y-4">
<textarea
name="comment"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Escreva seu comentário..."
className="w-full p-3 bg-gray-800 border border-gray-600 rounded-lg
text-white placeholder-gray-400 focus:border-lime-400
focus:outline-none resize-none"
rows={3}
/>
<button
type="submit"
disabled={!newComment.trim()}
className="px-6 py-2 bg-lime-500 text-black font-semibold rounded-lg
hover:bg-lime-400 disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
Comentar
</button>
</form>
</div>
);
Casos de Uso Ideais para useOptimistic
✅ Ideal Para:
- • Curtidas e reações
- • Comentários e mensagens
- • Adição ao carrinho
- • Favoritos e bookmarks
- • Atualizações de perfil
- • Toggles de configuração
❌ Evitar Em:
- • Transações financeiras
- • Envio de dados críticos
- • Operações irreversíveis
- • Sistemas com alta latência
- • Dados médicos sensíveis
- • Controle de acesso
Exemplo: Botão de Curtir Otimista
function LikeButton({ postId, initialLikes, isLiked }) {
const [likes, setLikes] = useState({ count: initialLikes, isLiked });
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(currentLikes, newLikedState) => ({
count: newLikedState
? currentLikes.count + 1
: currentLikes.count - 1,
isLiked: newLikedState
})
);
async function handleLike() {
const newLikedState = !optimisticLikes.isLiked;
// Atualização otimista instantânea
addOptimisticLike(newLikedState);
try {
await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
body: JSON.stringify({ liked: newLikedState })
});
// Confirma o estado após sucesso
setLikes({ count: optimisticLikes.count, isLiked: newLikedState });
} catch (error) {
// Em caso de erro, o estado reverte automaticamente
console.error('Erro ao curtir:', error);
}
}
return (
<button
onClick={handleLike}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all ${
optimisticLikes.isLiked
? 'bg-red-500/20 text-red-400 border border-red-500/30'
: 'bg-gray-800 text-gray-400 border border-gray-600 hover:border-red-500/50'
}`}
>
<span className={`text-lg ${optimisticLikes.isLiked ? 'animate-bounce' : ''}`}>
{optimisticLikes.isLiked ? '❤️' : '🤍'}
</span>
<span className="font-medium">{optimisticLikes.count}</span>
</button>
);
}
Tratamento de Erros e Rollback
Uma das grandes vantagens do useOptimistic é o rollback automático em caso de falha. Quando o estado base é atualizado, as mudanças otimistas são descartadas.
Estratégias de Erro Avançadas
function useOptimisticComments(initialComments) {
const [comments, setComments] = useState(initialComments);
const [error, setError] = useState(null);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{ ...newComment, status: 'sending' }
]
);
async function addComment(commentData) {
const tempId = crypto.randomUUID();
const optimisticComment = {
id: tempId,
...commentData,
timestamp: new Date().toISOString()
};
// Limpa erros anteriores
setError(null);
// Adiciona otimisticamente
addOptimisticComment(optimisticComment);
try {
const response = await fetch('/api/comments', {
method: 'POST',
body: JSON.stringify(commentData),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const savedComment = await response.json();
// Substitui o comentário otimista pelo real
setComments(prev => [...prev, savedComment]);
} catch (err) {
// Define erro (o rollback é automático)
setError(`Falha ao enviar comentário: ${err.message}`);
// Toast de erro opcional
showErrorToast('Comentário não foi enviado. Tente novamente.');
// O useOptimistic automaticamente remove o comentário
// otimista quando comments não é atualizado
}
}
return { optimisticComments, addComment, error };
}
Indicadores Visuais de Status
Estado Enviando: Animação de pulse, opacidade reduzida, ícone de loading
Estado Confirmado: Animação de sucesso, cores normais, ícone de check
Estado de Erro: Item removido + toast de erro + botão retry
Performance e Boas Práticas
Dicas de Performance:
- • Use
crypto.randomUUID()
para IDs temporários únicos - • Evite atualizar arrays grandes otimisticamente
- • Implemente debounce em operações frequentes
- • Use React.memo para componentes de lista
- • Considere virtualização para muitos itens
Comparação: Com vs Sem useOptimistic
❌ Implementação Manual
- • 50+ linhas de código
- • Gerenciamento de estado complexo
- • Propenso a bugs de sincronização
- • Rollback manual necessário
- • Difícil de testar
✅ Com useOptimistic
- • 15-20 linhas de código
- • Estado gerenciado automaticamente
- • Rollback automático em falhas
- • API simples e intuitiva
- • Fácil de testar e debuggar