DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Your Next.js Dates Are Wrong for Half Your Users (and the Hydration Warning Is Only the Start)

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>;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 reads window or localStorage can 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>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)