Back to the blog
ReactUXFrontend

A lying interface in React: the Theranos lesson

October 20, 2026·6 min read·Diego Horvatti

Theranos promised hundreds of blood tests from a single drop taken from your finger. For years, the most convincing part was the screen. The Edison device had a beautiful interface, a progress bar and a result at the end. Behind all that, almost nothing worked. Now there's a new documentary about Elizabeth Holmes, "You Can See Everything", and the newsletter Read The Profile published a harsh review: the film would be misleading in the same way the company was. It stuck with me for a very dev reason. A lot of lying interfaces in React come from the same temptation: showing that everything is fine before you know it is.

What happened with Theranos, in 30 seconds

If you didn't follow it: Theranos was valued at around 9 billion dollars. It partnered with Walgreens and put tests in real stores, with real patients. In 2015, journalist John Carreyrou, from the Wall Street Journal, showed that most tests ran on commercial machines from other manufacturers, modified. The Edison didn't deliver what the company claimed.

Holmes was convicted of defrauding investors in 2022 and got more than 11 years in prison. The new documentary goes back to the story, and the Read The Profile review says it packages the narrative in a way that leaves viewers with the wrong impression. I won't judge the film without seeing it. The point that interests me is a different one.

Carreyrou's own book, "Bad Blood", describes demos where the device showed on screen that it was working while the result, in practice, came from somewhere else. The interface was the product. And the interface lied.

Why a lying interface in React is so easy to write

Nobody wakes up wanting to defraud the user. The lie in the UI shows up little by little, out of rush or aesthetics. The PM asks to "make it feel smoother". The designer doesn't want an ugly error screen. You want to close the task before Friday.

That's when a few patterns show up that I've seen in code review more than once (and I've written a few myself, I admit):

  • A catch that swallows the error and shows a success toast anyway.
  • A progress bar animated by setInterval, with no connection to real progress.
  • An optimistic update that never rolls back when the API fails.
  • Stale cache data shown as if it were current, with no warning.
  • A skeleton that spins forever when the fetch breaks.

Each one looks harmless. Together, they create an app where users can no longer believe what they see.

A screen that always says "it worked" tells you nothing.

The swallowed error: the most common case

This one is a classic. The form saves, something goes wrong on the server, and the user gets confetti.

async function handleSave() {
  try {
    await api.updateProfile(form)
  } catch (e) {
    console.error(e) // nobody will read this in production
  }
  toast.success('Profile updated!')
}

The toast sits outside the try. It will show up no matter what. The user closes the tab happy, comes back tomorrow and the profile is the same. Now they don't trust any other toast in your app.

The honest version is boring and short:

async function handleSave() {
  try {
    await api.updateProfile(form)
    toast.success('Profile updated!')
  } catch {
    toast.error("Couldn't save. Your data is still in the form.")
  }
}

Look at the second sentence of the error. It says what happened and what wasn't lost. That's worth more than any cute "oops" illustration.

Optimistic update without rollback is Theranos in miniature

Optimistic updates are great. The like shows up instantly, the list reorders instantly, it feels like a native app. The problem is when optimism becomes a promise nobody keeps.

I've seen this pattern a lot:

function like() {
  setLikes((n) => n + 1)
  api.like(postId) // no await, no catch, no way back
}

If the request fails, the number stays wrong on screen until the next reload. The user saw a state that never existed in the database. It's literally the Edison progress bar: the screen claims something the system didn't do.

In React 19, useOptimistic solves this elegantly, because the optimistic value is temporary by definition. When the action finishes, it goes back to the real state:

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("Couldn't like this right now.")
  }
}

If the API fails, likes doesn't change, and optimisticLikes goes back to the true value on its own when the transition ends. The optimism is there, but it has an expiration date. With TanStack Query the reasoning is the same: onMutate saves the snapshot, onError restores it, onSettled invalidates and fetches again.

Fake loading and stale data: the silent lies

A fake progress bar is tempting because it makes waiting more "pleasant". There's even research saying users tolerate a moving bar better. Using an indeterminate animation is fine. The problem is the bar that climbs to 99% and gets stuck there for two minutes, because the number was made up from the start. Users learn fast that your 99% means nothing.

The rule I follow: if I don't have real progress, I use an indeterminate indicator. If I do (an upload with onprogress, a job with steps), I show the real number.

Stale data is more subtle. With a high staleTime or SWR, you show cache while revalidating. That's good for performance. But on a balance, inventory or order status screen, showing the value from 10 minutes ago with no signal is saying something false that looks true. A small "updated 10 min ago", or a discreet spinner in the corner while isFetching is true, already solves it.

const { data, isFetching, dataUpdatedAt } = useQuery({
  queryKey: ['balance'],
  queryFn: getBalance,
})

return (
  <p>
    $ {data?.value}
    {isFetching && <small> updating…</small>}
  </p>
)

What changes in your workflow

You don't need a new framework or library. You need a few habits:

  1. Every UI state needs a designed error state. If Figma doesn't have the error screen, the task isn't ready to start.
  2. Code review asks "what if it fails?" on every await. If the answer is "the user never finds out", it's a bug.
  3. Test the sad path on purpose. In DevTools, on the Network tab, you can block a specific URL or simulate offline. Five minutes doing this finds more problems than a week of happy-path testing.
  4. Optimistic only with rollback. Without rollback, it's not an optimistic update, it's a guess.

None of this is hard. It's just uncomfortable, because it forces you to admit on screen that the system sometimes fails. And it does fail, yours and everyone else's.

My take

The Theranos case is extreme, of course. Nobody goes to jail over a lying toast. But the mechanism is the same: the interface becomes a shop window and stops being a window. And the cost shows up the same way, just smaller and slower. First the user doubts a button, then the whole app, then your support team.

I'd rather have a slightly uglier UI that tells the truth than a beautiful UI I have to defend in an incident meeting. Users forgive errors. What they don't forgive is finding out the screen lied to them. If you want to see how I handle this in production, the apps are in my projects.

LinkedIn summary

Theranos didn't fool the world with blood. It fooled the world with a pretty screen and a progress bar.

I kept thinking about this after the new documentary about Elizabeth Holmes, and the lesson applies directly to anyone who writes frontend.

A success toast outside the try. An optimistic update that never rolls back. A progress bar that climbs to 99% and gets stuck there. I've seen all of these in code review, and I'll admit I've written a few myself.

Each one looks harmless. Together, they teach users not to believe anything they see on the screen.

I'd rather have a slightly uglier UI that tells the truth than a beautiful UI I have to defend in an incident meeting. Users forgive errors. They don't forgive finding out the screen lied.

I wrote the full article with React examples and useOptimistic. Link in the comments.

#React #Frontend #UX #WebDevelopment #CodeReview