When “Correct UI” Isn’t the Best UX: Rethinking a Login Button

Share this post on:

In theory, UI should always reflect the correct state.

In practice, that’s not always the best user experience.

I recently worked on what seemed like a small issue—a login/logout button flickering between “Log in” and “Log out.” But it turned into a deeper discussion about performance, correctness, and what actually matters in user experience.


The Problem

We had a header button that showed:

  • “Log in” when the user is logged out
  • “Log out” when the user is authenticated

But on page load, it briefly showed the wrong state.

Why?

Because authentication was resolved on the client.

The sequence looked like this:

  1. Initial render → assume logged out → show “Log in”
  2. Auth hydrates → user is actually logged in → update to “Log out”

On a fast connection, this flicker is barely noticeable.

On a slow connection?
It could take up to twenty seconds in some cases


The Initial Instinct: Fix the UI

The obvious solution was:

Don’t render the button or disable the button until we know the auth state.

Technically, this gives you correct UI.

But it introduces a new problem:

  • You’re now blocking interaction on auth resolution
  • On slow networks, the button may be unusable for a long time which could lead to user frustration clicks

So the question became:

Are we optimizing for correctness—or usability?


Why We Didn’t Solve This on the Server

At this point, the obvious “perfect” solution was:

Render the correct auth state on the server.

In theory, that would eliminate the flicker entirely.

We even explored it.

The idea was to convert the header into a server component so we could read authentication state (via cookies) during SSR and render the correct UI immediately.

But in practice, it wasn’t that simple.

The Constraints

  • The header is used across multiple applications as a client component
  • We’re using Next.js for our framework and it requires client components for children
  • Moving to server rendering would require significant architectural changes

We considered passing auth state down as props from the server.

But that introduced a bigger issue:

Every consuming application would now need to manually handle authentication state and integrate with our auth provider.

That’s not a small change—that’s a platform-level shift.


A Key Insight

Someone pointed out something that shifted the conversation:

Any client-side solution doesn’t remove the delay. It just hides it.

That forced us to rethink the goal.

Instead of asking:

“How do we make the UI always correct?”

We asked:

“What actually happens if the UI is temporarily wrong?”


The Solution: Optimize for Behavior

We changed the button from a pure action to a link to the login page.

And we let the login page handle the logic.

Now:

  • Logged-out user clicks → goes to login page (expected)
  • Logged-in user clicks → immediately redirected back (already authenticated)

Meanwhile:

  • If the user stays on the page, the button eventually updates to the correct state
  • “Log out” continues to behave as expected

Why This Works

We shifted our priority:

Before

Guarantee correct UI immediately

After

Guarantee correct behavior always

That distinction matters.

Because from the user’s perspective:

  • The system never breaks
  • The path forward is always valid
  • There are no dead ends or confusing states

Tradeoffs We Accepted

  • The label may briefly be incorrect
  • The UI becomes eventually consistent, not immediately accurate

But in exchange, we get:

  • Immediate interactivity
  • Resilience to slow connections
  • Simpler implementation without server-side auth rendering

Engineering Is About Constraints

This wasn’t really about a login button.

It was about how we think as engineers.

It’s easy to chase:

  • Perfect UI
  • Immediate correctness
  • Ideal conditions

But real systems operate under constraints:

  • Network latency
  • Asynchronous data
  • Imperfect timing
  • Multi-app shared components

The better question is:

What’s the worst outcome if this is temporarily wrong?

In our case:

A redirect that resolves instantly

That’s a safe failure.


Final Thought

Not every problem needs to be solved at the UI layer.

Sometimes the better solution is to make the system forgiving instead of perfect.

Because users don’t care if your state is technically correct at every millisecond.

They care that things work.


Welcome to my Engineering Lab—where small bugs turn into better systems thinking.


Share this post on:

Leave a Reply

Your email address will not be published. Required fields are marked *