Interface que mente no React: a lição da Theranos
A Theranos prometia centenas de exames de sangue com uma gota tirada do dedo. Por anos, a parte que mais convencia era a tela. O aparelho Edison tinha uma interface bonita, uma barra de progresso e um resultado no final, e por trás disso quase nada funcionava. Agora saiu um documentário sobre Elizabeth Holmes, "You Can See Everything", e a newsletter Read The Profile publicou uma crítica dura: o filme seria enganoso do mesmo jeito que a empresa foi. Fiquei com isso na cabeça por um motivo bem de dev. Muita interface que mente no React nasce da mesma tentação: mostrar que está tudo bem antes de saber se está.
O que aconteceu com a Theranos, em 30 segundos
Para quem não acompanhou: a Theranos chegou a valer cerca de 9 bilhões de dólares. Fechou parceria com a Walgreens e colocou exames em lojas de verdade, com pacientes de verdade. Em 2015, o jornalista John Carreyrou, do Wall Street Journal, mostrou que a maioria dos exames rodava em máquinas comerciais de outros fabricantes, adaptadas. O Edison não entregava o que a empresa dizia.
Holmes foi condenada por fraude contra investidores em 2022 e pegou mais de 11 anos de prisão. O documentário novo volta à história, e a crítica da Read The Profile diz que ele embala a narrativa de um jeito que deixa o espectador com a impressão errada. Não vou julgar o filme sem ter visto. O ponto que me interessa é outro.
O livro "Bad Blood", do próprio Carreyrou, descreve demos em que o aparelho mostrava na tela que estava trabalhando enquanto o resultado, na prática, vinha de outro lugar. A interface era o produto. E a interface mentia.
Por que uma interface que mente no React é tão fácil de escrever
Ninguém acorda querendo fraudar o usuário. A mentira na UI aparece aos poucos, por pressa ou por estética. O PM pede "deixa mais fluido". O designer não quer tela de erro feia. Você quer fechar a task antes da sexta.
Aí surgem uns padrões que eu já vi em code review mais de uma vez (e já escrevi alguns, confesso):
catchque engole o erro e mostra toast de sucesso mesmo assim.- Barra de progresso animada por
setInterval, sem nenhuma ligação com o progresso real. - Update otimista que nunca desfaz quando a API falha.
- Dado velho do cache exibido como se fosse atual, sem nenhum aviso.
- Skeleton que fica girando para sempre quando o fetch quebra.
Cada um parece inofensivo. Juntos, criam um app em que o usuário não consegue mais acreditar no que vê.
Uma tela que sempre diz "deu certo" não informa nada.
O erro engolido: o caso mais comum
Esse é clássico. O formulário salva, alguma coisa dá errado no servidor, e o usuário recebe confete.
async function handleSave() {
try {
await api.updateProfile(form)
} catch (e) {
console.error(e) // ninguém vai ler isso em produção
}
toast.success('Perfil atualizado!')
}
O toast fica fora do try. Vai aparecer de qualquer jeito. O usuário fecha a aba feliz, volta amanhã e o perfil está igual. Agora ele não confia em nenhum outro toast do seu app.
A versão honesta é chata e curta:
async function handleSave() {
try {
await api.updateProfile(form)
toast.success('Perfil atualizado!')
} catch {
toast.error('Não deu para salvar. Seus dados continuam no formulário.')
}
}
Repare na segunda frase do erro. Ela diz o que aconteceu e o que não se perdeu. Isso vale mais que qualquer ilustração fofa de "ops".
Update otimista sem rollback é Theranos em miniatura
Update otimista é ótimo. O like aparece na hora, a lista reordena na hora, a sensação é de app nativo. O problema é quando o otimismo vira promessa que ninguém cumpre.
Já vi esse padrão bastante:
function like() {
setLikes((n) => n + 1)
api.like(postId) // sem await, sem catch, sem volta
}
Se a requisição falhar, o número fica errado na tela até o próximo reload. O usuário viu um estado que nunca existiu no banco. É literalmente a barra de progresso do Edison: a tela afirma algo que o sistema não fez.
No React 19, o useOptimistic resolve isso de um jeito elegante, porque o valor otimista é temporário por definição. Quando a action termina, ele volta para o estado real:
const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, delta: number) => current + delta
)
async function likeAction() {
addLike(1)
try {
const updated = await api.like(postId)
setLikes(updated.likes)
} catch {
toast.error('Não foi possível curtir agora.')
}
}
Se a API falhar, likes não muda, e o optimisticLikes volta sozinho para o valor verdadeiro quando a transição acaba. O otimismo existe, mas tem prazo de validade. Com TanStack Query o raciocínio é o mesmo: onMutate guarda o snapshot, onError restaura, onSettled invalida e busca de novo.
Loading falso e dado velho: as mentiras silenciosas
Barra de progresso fake é tentadora porque deixa a espera mais "agradável". Tem até estudo dizendo que o usuário tolera melhor uma barra que se mexe. Tudo bem usar animação indeterminada. O problema é a barra que vai até 99% e trava ali por dois minutos, porque o número era inventado desde o começo. O usuário aprende rápido que o seu 99% não significa nada.
Regra que eu sigo: se eu não tenho progresso real, uso indicador indeterminado. Se tenho (upload com onprogress, job com etapas), mostro o número de verdade.
Dado velho é mais sutil. Com staleTime alto ou SWR, você exibe cache enquanto revalida. Isso é bom para performance. Mas em tela de saldo, estoque ou status de pedido, mostrar o valor de 10 minutos atrás sem nenhum sinal é dizer uma coisa falsa com cara de verdade. Um "atualizado há 10 min" pequeno, ou um spinner discreto no canto enquanto isFetching é true, já resolve.
const { data, isFetching, dataUpdatedAt } = useQuery({
queryKey: ['saldo'],
queryFn: getSaldo,
})
return (
<p>
R$ {data?.valor}
{isFetching && <small> atualizando…</small>}
</p>
)
O que muda no seu fluxo de trabalho
Não precisa de framework novo nem de lib. Precisa de alguns hábitos:
- Todo estado de UI tem que ter um estado de erro desenhado. Se o Figma não tem a tela de erro, a task não está pronta para começar.
- Code review pergunta "e se falhar?" em todo
await. Se a resposta for "o usuário não fica sabendo", é bug. - Teste o caminho triste de propósito. No DevTools, aba Network, dá para bloquear uma URL específica ou simular offline. Cinco minutos fazendo isso acham mais problema que uma semana de teste feliz.
- Otimista só com rollback. Sem rollback, não é update otimista, é chute.
Nada disso é difícil. É só desconfortável, porque obriga a admitir na tela que o sistema às vezes falha. E falha mesmo, o seu e o de todo mundo.
Minha opinião
O caso Theranos é extremo, claro. Ninguém vai preso por um toast mentiroso. Mas o mecanismo é o mesmo: a interface vira vitrine e para de ser janela. E o custo aparece do mesmo jeito, só que menor e mais lento. Primeiro o usuário duvida de um botão, depois do app inteiro, depois do seu suporte.
Eu prefiro uma UI um pouco mais feia que diz a verdade a uma UI linda que eu preciso defender em reunião de incidente. Usuário perdoa erro. O que ele não perdoa é descobrir que a tela mentiu para ele. Se quiser ver como eu lido com isso em produção, os apps estão em meus projetos.
Resumo para o LinkedIn
A Theranos não enganou o mundo com sangue. Enganou com uma tela bonita e uma barra de progresso. Fiquei pensando nisso com o documentário novo sobre a Elizabeth Holmes, e a lição serve direto para quem escreve frontend. Toast de sucesso fora do try. Update otimista que nunca desfaz. Barra de progresso que vai até 99% e trava ali. Já vi tudo isso em code review, e confesso que já escrevi alguns. Cada um parece inofensivo. Juntos, ensinam o usuário a não acreditar em nada do que aparece na tela. Prefiro uma UI um pouco mais feia que diz a verdade a uma UI linda que eu preciso defender em reunião de incidente. Usuário perdoa erro. Não perdoa descobrir que a tela mentiu. Escrevi o artigo completo com os exemplos em React e o useOptimistic. Link nos comentários. #React #Frontend #UX #DesenvolvimentoWeb #CodeReview