You format a date in a component. It looks right on your laptop. Then a user in another country opens the page and either sees a console full of hydration errors, or worse, sees nothing wrong at all and reads the wrong time.
I live in a timezone that is five hours away from where most servers run, so this one finds me every time.
The Code That Looks Fine
// components/OrderDate.tsx
export function OrderDate({ date }: { date: string }) {
return <p>Ordered on {new Date(date).toLocaleString()}</p>;
}
No hooks, no browser APIs, nothing that looks risky. It even works in next dev, because on your machine the server and the browser are the same computer.
Why It Breaks
In Next.js, this component runs twice: once on the server to produce the HTML, and again in the browser to hydrate it. toLocaleString() with no arguments uses the timezone and language of whatever machine runs it.
- The server is often in UTC, with a default locale such as
en-US. - Your user's browser might be in Karachi, with
en-GB.
Server HTML: Ordered on 10/20/2026, 10:15:00 AM
Browser render: Ordered on 20/10/2026, 15:15:00
React compares the two, finds that they differ, and logs a hydration error. In development, you see it. In production, React quietly tries to recover by re-rendering on the client, which costs time and can cause a visible flash.
Why It Is Hard to Catch
- It never fails where you test. Same machine, same timezone, same locale: the two outputs are identical.
- Many users never see an error. The page just shows one format for a split second and then another.
-
It often hides inside other pieces of data.
Date.now(),Math.random(),new Date().getFullYear()and anything that readswindoworlocalStoragecan all produce a different value on the server and in the browser.
The Fix That Is Tempting, and Wrong
You will find this advice everywhere:
<p suppressHydrationWarning>
Ordered on {new Date(date).toLocaleString()}
</p>
It does silence the warning. But per React's documentation, it only suppresses the warning on that one element and does not patch up mismatched text. The user can end up looking at the server's version of the time, which is in the wrong timezone.
You did not fix the bug, you removed the alarm for it.
Fix 1: Make the Output the Same on Both Sides
Pass an explicit locale and timezone, so the server and the browser calculate the same string:
export function OrderDate({ date }: { date: string }) {
const text = new Date(date).toLocaleString('en-US', {
timeZone: 'UTC',
dateStyle: 'medium',
timeStyle: 'short',
});
return <time dateTime={date}>{text} UTC</time>;
}
This is deterministic, so there is nothing to mismatch. It is the right choice for logs, admin tables and anything where showing UTC is acceptable. Label it as UTC so nobody reads it as local time.
Fix 2: Show the Local Time After the Page Loads
If users should see their own timezone, only compute it in the browser:
'use client';
import { useEffect, useState } from 'react';
export function LocalDate({ date }: { date: string }) {
const [local, setLocal] = useState<string | null>(null);
useEffect(() => {
setLocal(
new Date(date).toLocaleString(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
})
);
}, [date]);
return (
<time dateTime={date}>
{local ?? new Date(date).toISOString().slice(0, 10)}
</time>
);
}
The first render, on the server and in the browser, shows a stable fallback (the ISO date). Right after hydration, useEffect runs only in the browser and swaps in the local time. Nothing mismatches, and the user ends up with the right value.
The cost is a brief change from the fallback to the local string. Keep the fallback short so the layout does not jump.
Fix 3: Store and Send Dates as ISO Strings
Whichever display option you pick, pass dates around as ISO strings (2026-10-20T10:15:00.000Z) and format them only at the edge, in one place. An ISO string is plain text, so it means the same thing on the server, in the browser and in your database.
Which One When
| Situation | Fix |
|---|---|
| Admin tables, logs, reports | Fixed locale and timeZone: 'UTC'
|
| Customer-facing times (orders, bookings, messages) | Format in the browser after mount |
| A date that never changes (blog post date) | Fixed locale and timezone, since the author's date is what matters |
The Rule
Any value that depends on where the code runs, such as timezone, locale, the current time or a random number, will differ between the server and the browser. Either make it identical on both sides, or compute it only in the browser after hydration. Do not suppress the warning and hope.
Quick audit: search your project for toLocaleString(, toLocaleDateString(, Date.now() and Math.random() inside components that render on the server. Each one is either deterministic on purpose, or a mismatch that has not met a user in another timezone yet.
Hydration mistakes also slow pages down, because React has to redo work in the browser. I explain what that does to real-world speed scores in this plain-language guide to Core Web Vitals, and I keep every date in my Next.js templates formatted through one small helper so the server and the browser always agree.
Get the templates: https://pixelanas.gumroad.com
Which timezone bug has bitten you hardest? Drop it in the comments, especially if it only showed up in production.
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)