Blog #4: Hydration Error – When SSR's perfection 'slaps' me in the face at midnight
The story of 6 hours struggling with a Hydration error in Next.js, from blaming the API to realizing I was too naive in handling state between Server and Client.
I still vividly remember that bone-chilling feeling at 2 AM that day. The whole team was preparing for the biggest release of the quarter. Everything on my local machine was running as smooth as silk. Pull Requests were merged, and CI/CD was all green. But the moment I hit the Deploy button for Staging, the browser suddenly slapped a bright red line in my face in the console:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
At that moment, our Dashboard—a real-time token tracking system for an exchange—suddenly went blank. Or worse, it flickered constantly like a dying light bulb. The deadline was only 6 hours away from the launch time. The pressure wasn't "how to make it run," but "why is it wrong when everything seemed perfect?"
1. "Hydration" – When Server and Client don't understand each other
To understand why I lost sleep that night, we need to talk a little bit about how Next.js (or any SSR framework) works.
Imagine the Server is a chef preparing a salad (static HTML) and sending it to your table. When the food arrives, "Hydration" is like sprinkling on spices and toppings (JavaScript/Event Listeners) to make the dish come alive and interactive.
The problem arises when the Server prepares a salad with tomatoes, but when it reaches the Client (browser), JavaScript insists the plate must have... cucumbers. React recognizes this difference, panics, wipes everything clean, and re-renders from scratch. The result? A slight "flicker" or worse, the entire UI crashes.
That's the theory, but in reality, it's much more subtle and insidious.
2. The Problem: A "Real-time" Dashboard or a "Real-time" Disaster?
The system I was working on was quite large. We combined Server-side Rendering for SEO data of various tokens and Client-side Side Effects to update fluctuating values every second.
The issue lay in a seemingly simple component: Cart Summary. It displayed total assets and... the last update time.
// My naive code at the time
export const LastUpdated = () => {
const now = new Date().toLocaleTimeString();
return <div>Updated at: {now}</div>;
};
On my machine, everything was fine. But on the Server, new Date() grabs the server's timezone (usually UTC). When it reaches the user's browser in Vietnam, new Date() grabs GMT+7.
Boom! Server says 7:00 PM, Client says 2:00 AM. React sees the discrepancy, and Hydration fails. The 150 articles I once read about Next.js couldn't save me from this carelessness.
3. The Debug Process: Wrong assumptions
Initially, I was convinced the error was with the API. I thought the data returned from the server was cached old data, while the client fetched new data, causing a mismatch. I spent 2 hours checking Backend logs, scrutinizing every byte in the Network tab.
Everything matched.
Then I shifted my suspicion to third-party libraries. "Maybe this Chart isn't rendering correctly?". I started commenting out components one by one. This process was extremely time-consuming.
The lifesaver that night was the React Profiler and enabling Strict Mode. I noticed that only the tiny area displaying the time was causing the error. It finally clicked: I was trying to render something "non-deterministic" directly on the Server.
4. The Fix: From "Quick Fix" to "Sustainable"
In the panic, a Junior on the team suggested: "Hey, why don't we use suppressHydrationWarning? It's quick".
Yes, that's what a lot of people choose. It's like seeing a crack in the wall and hanging a picture to hide it. The error doesn't show up anymore, but the asynchronous nature remains. If that were financial data, displaying a mismatch between Server and Client would be a recipe for disaster.
I chose a more sustainable way, even though it cost a few more lines of code. That's ensuring that rendering environment-sensitive data only happens after it has "taken root" (mounted) on the client.
// Sustainable fix
export const LastUpdated = () => {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return <div className="skeleton-loader" />; // Render placeholder on server
const now = new Date().toLocaleTimeString();
return <div>Updated at: {now}</div>;
};
This ensures the Server-side always renders an identical placeholder for every user. Only when the JavaScript is downloaded on the Client side does it calculate the actual value.
5. Counter-argument: Am I Over-engineering?
Sometimes I wonder: Is it really necessary to complicate a timestamp like that? Why not just use ssr: false for the entire component through next/dynamic?
Actually, if it were just a personal blog, using suppressHydrationWarning probably wouldn't hurt anyone. But when you're working on large systems where every pixel matters, understanding the Hydration lifecycle helps you control Performance. Re-rendering the entire DOM tree just because of a mismatched date is a massive waste of resources on mobile devices.
6. Lessons Learned
After 6 hours of struggle and finally succeeding with the release at dawn, I realized 3 things:
- Never trust Localhost: Your Local environment is too ideal. It doesn't have the Server's network latency or Production Container's timezone differences.
- Server is "Fixed", Client is "Variable": Anything that depends on
window,localStorage,Date(), orMath.random()must be handled with extreme care in SSR. - Quick fix is poison: A quick solution only helps you sleep tonight but will keep you up all of next week when the bug spreads to other logic using that component.
Modern Frontend is no longer just about making an interface look beautiful. It's about managing data synchronization between different environments. If you see a Hydration error, don't rush to comment out code. Stop and ask: "Can my Server know this?". If the answer is no, let the Client handle it.
Notes from a sleepless night.
Series • Part 4 of 50