# Hydration mismatch da date: il server è in UTC, il browser no

La stessa data formattata sul server e sul client produce stringhe diverse quando cambiano fuso orario e locale. La correzione è formattare una volta sola, sul server, e passare la stringa.

> Pagina: https://ui.eneawork.it/blog/hydration-mismatch-date
> Markdown: https://ui.eneawork.it/blog/hydration-mismatch-date.md

*2026-11-01*

Risposta breve: il server formatta la data in UTC, il browser nel fuso dell'utente. Le due stringhe differiscono e React rigenera l'albero. Formatta una volta sola, sul server, e passa la stringa già pronta.

La domanda «React 18: Hydration failed because the initial UI does not match what was rendered on the server» vale 341 voti su Stack Overflow (letto il 31 ottobre 2026 con la Stack Exchange API): fonte. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, verificati su /api/stats il 1 novembre 2026. Ogni comando qui sotto è eseguibile in locale: nessun passaggio richiede un account.

## Dove nasce la differenza

In SSR il server produce HTML con il testo già dentro. Durante l'idratazione React esegue di nuovo il componente, costruisce l'albero in memoria e confronta nodo per nodo. Se il testo differisce, React considera rotto il confine e rigenera quel sottoalbero nel client: il DOM del server viene buttato, insieme allo stato dei campi e al lavoro di render.

Sulla data concorrono tre variabili, e la terza è la più insidiosa. Fuso e locale li conosci; la versione di ICU no. Node e browser possono avere dati CLDR diversi e produrre separatori diversi per lo stesso istante, per esempio uno spazio unificatore prima di AM. La stringa sembra identica a schermo e differisce per un code point.

## Perché le date sono il caso peggiore

Una data è il risultato di tre variabili: il timestamp, il fuso orario e il locale. Il timestamp è uguale, le altre due no. Il server quasi sempre gira in UTC e in inglese; il browser usa il fuso e la lingua del sistema.

new Date("2026-11-01T09:00:00Z").toLocaleString() // server (UTC, en-US): "11/1/2026, 9:00:00 AM" // client (Europe/Rome): "01/11/2026, 10:00:00"

## La correzione preferita: formatta sul server

// app/orders/[id]/page.tsx — Server Component const DF = new Intl.DateTimeFormat("it-IT", { dateStyle: "long", timeZone: "Europe/Rome", }); export default async function Page({ params }) { const order = await getOrder(params.id); return ( {DF.format(new Date(order.createdAt))} ); }

La stringa arriva già formattata: il client non ricalcola nulla. Il fuso è esplicito e non dipende dalla macchina che esegue il render.

## La correzione quando il fuso deve essere quello dell'utente

"use client"; import { useEffect, useState } from "react"; export function LocalDate({ iso }: { iso: string }) { const [label, setLabel] = useState(null); useEffect(() => setLabel(new Date(iso).toLocaleString()), [iso]); return ( {label ?? "—"} ); }

Il primo render mostra un trattino su server e client, quindi coincide. Dopo il mount arriva la data locale. Il suppressHydrationWarning serve solo per il caso limite in cui il browser idrata più tardi di quanto previsto.

## Il passo diagnostico

Il messaggio di React contiene già le due stringhe: cercalo nella console e confronta i code point, non i caratteri visibili.

const iso = "2026-11-01T09:00:00Z"; const f = (tz) => new Intl.DateTimeFormat("it-IT", { dateStyle: "long", timeStyle: "short", timeZone: tz }).format(new Date(iso)); for (const v of [f("UTC"), f("Europe/Rome")]) console.log(JSON.stringify(v), [...v].map((c) => c.codePointAt(0).toString(16)).join(" "));

Esegui lo stesso blocco in Node e nella console del browser: se le sequenze divergono con lo stesso fuso, il problema sono i dati di localizzazione, non il tuo codice. Allineare la versione di ICU fra i due ambienti, o smettere di formattare sul client, è l'unica strada pulita.

## Quando la correzione non funziona

Se il mismatch resta, cerca altre fonti di variazione nella stessa pagina: Math.random(), Date.now(), un contatore, un id generato senza useId. Spesso la data è solo il primo sintomo. Un secondo caso è un'estensione del browser che riscrive il testo: verifica in finestra anonima.

## Quando la correzione comune peggiora le cose

suppressHydrationWarning è la prima riga che si trova cercando l'errore. Silenzia l'avviso per un livello di annidamento e lascia il DOM del server in pagina: l'utente vede l'ora del server finché un aggiornamento di stato non la sostituisce, e il difetto resta invisibile nei log. Su un <time> è accettabile perché il valore cambia davvero dopo il mount; su un wrapper che contiene altri testi nasconde anche i mismatch veri.

L'altra correzione che si legge spesso è fissare il fuso a UTC per far coincidere le stringhe. L'errore sparisce e il risultato è sbagliato per ogni utente fuori da Greenwich: un ordine fatto alle dieci del mattino appare alle otto. Un mismatch visibile è meglio di una data sbagliata in silenzio.

## Un limite da mettere nel conto

Il server non può conoscere il fuso dell'utente alla prima richiesta. L'unico indizio nell'header è Accept-Language, che dice la lingua, non il fuso: due utenti in italiano possono stare a Roma e a San Paolo. Con HTML statico o cache CDN la personalizzazione al primo byte non esiste, e la data locale richiede un secondo passaggio sul client. Il trattino iniziale non è una scelta estetica, è l'unica cosa che server e client possono disegnare allo stesso modo.

## Come prevenirlo

## Dove continuare

Nel catalogo: componenti, data table e blocchi. Le guide: hydration failed: cause e fix e struttura App Router; il confronto: dark mode senza secondo design system.

## FAQ

### Perché la build non segnala il problema?

Il mismatch nasce dal confronto tra HTML del server e render del client: il compilatore non ha due ambienti da confrontare.

### Basta usare UTC ovunque?

Per un timestamp tecnico sì. Per una data mostrata a una persona no: l'utente si aspetta il proprio fuso. Formattala sul client dopo il mount.

### `` risolve?

No, ma è la forma corretta: l'attributo porta la data leggibile dalla macchina, il testo quella leggibile dalla persona.

### Come testo il caso?

Imposta il fuso del browser su un valore diverso da quello del server e apri la pagina: il mismatch appare subito.

### Vale per i numeri?

Sì: separatori delle migliaia e decimali cambiano con il locale. La regola è la stessa, formatta una volta sola.
