Hydration Proof

Search documentation

Find a page or section

Fix the toLocaleString hydration mismatch

1,234.5 on the server, 1.234,5 in Berlin.

A toLocaleString hydration mismatch happens when the server and browser use different default locales. The same number can render as "1,234.5" on one side and "1.234,5" on the other. Choose a locale from the URL, a cookie, or request data, then pass it to both renders.

Symptoms

Hydration failed because the server rendered text didn't match the client.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "Total: 1,234,567.891" Client: "Total: 1.234.567,891"
Minified React error #418; visit https://react.dev/errors/418

A toLocaleTimeString hydration error can show "5:00:00 PM" on the server and "17:00:00" in the browser. React also lists locale-dependent date formatting among possible causes.

hydration-proof reports changed text as HP1001. The same digits with different separators are a clue that formatting changed.

Why a toLocaleString hydration mismatch happens

When you omit a locale, each runtime chooses its own default. The server's container and the visitor's browser may not choose the same one.

These use the default locale:

  • value.toLocaleString(), toLocaleDateString() and toLocaleTimeString()
  • a.localeCompare(b) without its second argument
  • Intl.NumberFormat, Intl.DateTimeFormat, Intl.RelativeTimeFormat, Intl.PluralRules, Intl.Collator, Intl.ListFormat and Intl.DisplayNames without a locale, or with undefined
  • Intl.NumberFormat().resolvedOptions().locale, which returns the default itself

For dates, set the timezone too. An explicit locale alone does not prevent a timezone mismatch.

How to fix it

Pass the locale from the request

Read the locale from the URL, a cookie, or the request. Pass it to the component that formats the value:

app/[locale]/pricing/page.tsx
import { Price } from "./price";
 
export default async function Page({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  return <Price amount={1234.5} locale={locale} />;
}
app/[locale]/pricing/price.tsx
"use client";
 
export function Price({ amount, locale }: { amount: number; locale: string }) {
  // Before: amount.toLocaleString() used each runtime's default
  const text = amount.toLocaleString(locale, {
    style: "currency",
    currency: "EUR",
  });
  return <p>Total: {text}</p>;
}

Pin one locale where it does not matter

If the product uses one locale, set it explicitly everywhere:

percent.tsx
const percent = new Intl.NumberFormat("en-US", { style: "percent" });
 
export function Percent({ value }: { value: number }) {
  return <span>{percent.format(value)}</span>;
}

Also pass a locale to localeCompare. Otherwise a sorted list can appear in a different order. See require-deterministic-list-order.

Fix an i18n hydration error in next-intl

A next-intl hydration error can come from different locale, timezone, or "now" values. Set them together in the request config:

src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
 
export default getRequestConfig(async () => ({
  locale: "en",
  // One zone for both sides. Read it from the user's profile or a cookie if it varies.
  timeZone: "Europe/Berlin",
  // One "now" for both sides, used by relative time formatting.
  now: new Date(),
}));

When a Server Component renders NextIntlClientProvider, it passes the request configuration to the client. If you render the provider in a Client Component, pass the values it needs yourself. See next-intl's date-formatting guide.

Format after mount when only the browser knows

If the server cannot know the browser's locale, render a stable value first and format after hydration. See two-pass rendering.

Catch it with ESLint

no-locale-without-explicit-locale flags formatting calls without a locale. It accepts a variable such as toLocaleString(locale). Choose the right locale for your users rather than relying on a runtime default.

npm install -D eslint-plugin-hydration-proof

Catch it in CI

Add the locales your visitors use. A local test with only your machine's locale may miss the problem:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  matrix: {
    locale: ["en-US", "de-DE", "ar-EG"],
  },
});

The scenario also sets Accept-Language. Use --probe to change only the browser locale and see whether the finding follows it. See probes.

npx hydration-proof test --probe