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:
- Initial render → assume logged out → show “Log in”
- 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.