Voltar para o blog
ReactUXFrontend

Interface que mente no React: a lição da Theranos

20 de outubro de 2026·6 min de leitura·Diego Horvatti

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):

  • catch que 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:

  1. 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.
  2. Code review pergunta "e se falhar?" em todo await. Se a resposta for "o usuário não fica sabendo", é bug.
  3. 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.
  4. 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