Search... ⌘K
Write Log In Sign Up

Join DevSolved

Share war stories & investigate outages.

Sign Up for Free Log In to Account
ESC
HIGH Resolved

React Hydration Mismatch: The "Text content did not match" Nightmare

Avatar Devsolved Mar 12, 2024 — 09:00 UTC MTTR: 6 hours 2 min read 89001 views
Executive Summary

A seemingly harmless Date object rendered differently on the server versus the client, causing React 18 to discard the entire server-rendered DOM and rebuild it from scratch, tanking our Core Web Vitals.

The Symptom

Symptom

After migrating our e-commerce storefront to Next.js with React 18, our Cumulative Layout Shift (CLS) scores spiked. Users complained that the page would visually "flash" white for a split second right after loading. The browser console was flooded with `Warning: Text content did not match. Server: "10:30 AM" Client: "14:30 PM"`.

Root Cause Analysis

Root Cause

We were displaying the user's local time for an upcoming flash sale using `new Date().toLocaleTimeString()`. The Next.js server rendered the page in UTC ("10:30 AM"). When the React bundle hydrated on the client's browser (which was in EST), the client calculated "14:30 PM". React 18's strict hydration caught the mismatch. Because it couldn't reconcile the differences, it bailed out of hydration and completely destroyed and re-rendered the entire component tree.

Code javascript
// The fatal flaw: Relying on browser-specific state during initial render
const FlashSaleTimer = () => {
  const [time, setTime] = useState(new Date().toLocaleTimeString());
  return <div>Sale ends at: {time}</div>;
};

Resolution

Resolution

We implemented a `useMounted` hook to suppress the client-side time rendering until *after* hydration was complete. During SSR and the initial hydration pass, the component renders a generic "Loading..." skeleton. Only after the `useEffect` fires does it read the browser's timezone and swap the text.
Code javascript
// The fix: Two-pass rendering
const FlashSaleTimer = () => {
  const [isMounted, setIsMounted] = useState(false);
  useEffect(() => setIsMounted(true), []);

  if (!isMounted) return <div>Sale ends at: --:--</div>;
  return <div>Sale ends at: {new Date().toLocaleTimeString()}</div>;
};

Preventive Action Items

high Audit codebase for other browser-specific API calls in initial state @Frontend Team
normal Enable ESLint `react-hooks/exhaustive-deps` strictly @QA Team
|

Discussions 0

Most recent