- Published on
Text content does not match server-rendered HTML: Dates, Locales, and Random Values
- Authors

- Name
- Salman
- X
- @lamebrowndev
The error
Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML.
You have seen this. Everyone has seen this. It is the most common hydration error in Next.js and the error message tells you almost nothing useful. No file name. No line number. Just "text content does not match" and a vague hint about the server.
I wrote about hydration errors in general in my last post. This one is about the specific case where text content does not match. In my experience, 90 percent of these are caused by three things: dates, locales, and random values. Let me go through each one and show you the fix.
Why text mismatches happen
React hydration works by comparing the HTML that the server generated with the HTML that the client generates on the first render. If any text node is different, even by a single character, React throws this error.
The key insight is that React compares the exact string output. Not the DOM structure, not the props, the actual text. So if the server renders "July 4, 2025" and the client renders "Jul 4, 2025", that is a mismatch. If the server renders "1,234.56" and the client renders "1.234,56", that is a mismatch.
This is why dates, locales, and random values are the usual suspects. They produce different text on the server and the client.
Cause 1: Dates formatted with the user's locale
This is the number one cause. You render a date and format it with toLocaleDateString() or toLocaleString() or toLocaleTimeString(). The server formats it one way. The client formats it another way. Mismatch.
export default function Post({ date }: { date: string }) {
return <p>Published on {new Date(date).toLocaleDateString()}</p>
}
On the server, the default locale might be en-US and you get "7/4/2025". On the client, the user's browser might be set to en-GB and you get "04/07/2025". Different text. Hydration error.
Even if the server and client are both in en-US, the timezone can cause a difference. The server might be in UTC. The client might be in PST. A date that is July 4 in UTC is July 3 in PST.
Fix: Use explicit locale and timezone
The simplest fix is to specify the locale and timezone explicitly so both server and client produce the same string.
export default function Post({ date }: { date: string }) {
const formatted = new Date(date).toLocaleDateString('en-US', {
timeZone: 'UTC',
year: 'numeric',
month: 'long',
day: 'numeric'
})
return <p>Published on {formatted}</p>
}
Now both server and client render "July 4, 2025". No mismatch.
The downside is that every user sees the date in en-US format and UTC timezone. That might be fine for a blog. It might not be fine for an app where users expect their local format.
Fix: Format on the client only
If you need the user's locale and timezone, render a static placeholder on the server and format the date only after hydration.
'use client'
import { useState, useEffect } from 'react'
export default function PostDate({ date }: { date: string }) {
const [formatted, setFormatted] = useState<string | null>(null)
useEffect(() => {
setFormatted(new Date(date).toLocaleDateString())
}, [date])
// Server renders the ISO string. Client also renders it initially.
// Then useEffect runs and updates with the formatted version.
return (
<time suppressHydrationWarning>
{formatted ?? new Date(date).toISOString()}
</time>
)
}
The suppressHydrationWarning prop tells React to not check this element for mismatches. The server renders the ISO string. The client also renders the ISO string on the first render (because formatted is null). Then useEffect runs and updates to the formatted version. The user sees their local format after a brief flash of the ISO string.
Fix: Use a date formatting library
If you are formatting dates in a lot of places, use a library like date-fns or dayjs. They let you format dates without relying on the system locale.
import { format } from 'date-fns'
export default function Post({ date }: { date: string }) {
return <p>Published on {format(new Date(date), 'MMMM d, yyyy')}</p>
}
date-fns format is deterministic. The same input always produces the same output regardless of locale. No mismatch.
Cause 2: Number formatting with locales
Same story as dates but with numbers. You use toLocaleString() to format a number and the server and client disagree.
export default function Price({ amount }: { amount: number }) {
return <p>{amount.toLocaleString()}</p>
}
On the server in en-US: "1,234.56". On the client in de-DE: "1.234,56". Mismatch.
Fix: Specify the locale
export default function Price({ amount }: { amount: number }) {
return <p>{amount.toLocaleString('en-US')}</p>
}
Or use Intl.NumberFormat:
const formatter = new Intl.NumberFormat('en-US')
export default function Price({ amount }: { amount: number }) {
return <p>{formatter.format(amount)}</p>
}
Fix: Format on the client only
Same pattern as dates. Render a plain number on the server, format after hydration.
'use client'
import { useState, useEffect } from 'react'
export default function Price({ amount }: { amount: number }) {
const [formatted, setFormatted] = useState(amount.toString())
useEffect(() => {
setFormatted(amount.toLocaleString())
}, [amount])
return <span suppressHydrationWarning>{formatted}</span>
}
Cause 3: Math.random() and Date.now()
Any function that returns a different value on each call will cause a mismatch if it runs during render on both server and client.
export default function Banner() {
return <div className={`banner-${Math.random()}`}>Hello</div>
}
The server generates one random number. The client generates another. The className is different. Mismatch.
Date.now() is the same. The server renders one timestamp. The client renders another a few milliseconds later. Different text.
Fix: Move to useEffect
Generate the dynamic value only on the client, after hydration.
'use client'
import { useState, useEffect } from 'react'
export default function Banner() {
const [id, setId] = useState('banner')
useEffect(() => {
setId(`banner-${Math.random()}`)
}, [])
return <div className={id}>Hello</div>
}
The server renders with id = 'banner'. The client also starts with 'banner'. No mismatch. Then useEffect updates the id. If you use the id for CSS, the class changes after hydration. Usually fine.
For Date.now():
'use client'
import { useState, useEffect } from 'react'
export default function Clock() {
const [now, setNow] = useState<number | null>(null)
useEffect(() => {
setNow(Date.now())
const interval = setInterval(() => setNow(Date.now()), 1000)
return () => clearInterval(interval)
}, [])
if (now === null) {
return <span suppressHydrationWarning>--:--:--</span>
}
return <span>{new Date(now).toLocaleTimeString()}</span>
}
Cause 4: UUID and other random generators
If you generate IDs with crypto.randomUUID() or uuid() during render, you get a different value on server and client.
import { v4 as uuidv4 } from 'uuid'
export default function Form() {
const id = uuidv4()
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} />
</>
)
}
Fix: Use React's useId hook
React 18 introduced useId which generates stable IDs that are the same on server and client.
import { useId } from 'react'
export default function Form() {
const id = useId()
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} />
</>
)
}
The ID is generated by React in a way that is deterministic between server and client. No mismatch.
If you need a random ID for something that is not a React element (like a database ID), generate it in useEffect or in a Server Action, not during render.
Cause 5: Content from an API that changes between render and hydration
This is sneakier. You fetch data in a Server Component and the data changes between the time the server renders and the time the client hydrates.
export default async function Page() {
const views = await getViews()
return <p>This post has {views} views</p>
}
The server fetches "42 views" and renders it. By the time the client hydrates (which could be seconds later on a slow connection), the view count is now "43". The text does not match.
Fix: Wrap in Suspense
If the data is dynamic, isolate it in a Suspense boundary. React knows it might change and does not treat it as a mismatch.
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<p>Loading views...</p>}>
<Views />
</Suspense>
)
}
async function Views() {
const views = await getViews()
return <p>This post has {views} views</p>
}
Fix: suppressHydrationWarning for truly dynamic content
If you have a counter or a live value that you know will be different, use suppressHydrationWarning:
export default async function Page() {
const views = await getViews()
return <p suppressHydrationWarning>This post has {views} views</p>
}
Use this sparingly. It silences the warning but also hides real mismatches in that subtree.
How to debug these
The error message does not tell you which component caused the mismatch. Here is my process.
Step 1: Look at the error detail. The error sometimes includes a snippet of the mismatched text. Something like "server: July 4, 2025, client: Jul 4, 2025". That tells you it is a date formatting issue.
Step 2: View page source. Right click, view page source. That is the server HTML. Search for the text that you see in the browser. The difference between the source and the browser is your mismatch.
Step 3: Search your codebase for the usual suspects. Grep for toLocaleDateString, toLocaleString, Math.random, Date.now, uuid. Each one is a candidate.
Step 4: Comment out components. If you cannot find it, comment out components one at a time until the error goes away. Start with the ones that render dates or numbers.
Step 5: Check third-party libraries. Some UI libraries format dates or numbers internally. Check the props you pass to them. If a date picker formats the date differently on server and client, that is your mismatch.
A quick checklist
Before you ship, run through this:
- Every
toLocaleDateStringcall has an explicit locale - Every
toLocaleStringcall has an explicit locale - No
Math.random()in render - No
Date.now()in render - No
uuid()orcrypto.randomUUID()in render, useuseIdinstead - Dynamic data from APIs is wrapped in Suspense
- Test in a browser with a different locale than your server
That last one is important. If your server is in en-US and you test in a browser set to en-US, you will never catch locale mismatches. Change your browser locale to de-DE or ja-JP and load your app. If anything breaks, you found your mismatches.
I fix Next.js production issues for a living. If you have a hydration error you cannot track down, email me.
