はじめに
はじめまして、Web開発を初めて半年のしょーへいです。
現在、 React や Next.js 、Prismaを使用して「筋トレ記録App」を作成しています。
今回はApp作成中に発生した"Hydration Error"について備忘録の意味を込めて記事にしてみようと思います。
わからないことが多いので、アドバイスなどいただけると嬉しいです。
事象
筋トレ記録Appで、「カレンダーで選択した日と同じ日に行ったトレーニング内容を表示する機能」を作成していました。
以下の様なソースコードを書いたところ、"Hydration Error"が発生しました。
import Calendar from "react-calendar";
export default function CalendarFunction (){
const [date, setDate] = useState(new Date()); //初期値で実行時の時間をセット
・・・
return(
<>
<Calendar
locale="ja-JP"
onChange={(value) => setDate(value as Date)}
value={date}
/>
<p className="pt-4 text-center">
{date.toLocaleDateString("ja-JP", {
year: "numeric",
month: "numeric",
day: "numeric",
})}{" "}
の記録
</p>
</>
);
}
[browser] Uncaught Error: Hydration failed because the server rendered text didn't match the client.
原因候補
今回の"Hydration Error"は、
new Date() による初期表示の差分と、
react-calendar のSSR時の描画差分が原因候補でした。
"Hydration"とは、
サーバーが作ったHTMLに、ブラウザでReactの機能を結びつける処理のことです。
つまり、"Hydration Error"とは、
サーバーで作ったHTMLとブラウザ側で描画したHTMLの内容が一致していないことです。
補足:
ブラウザとは、
ChromeやSafariといったユーザーが操作するユーザーインターフェイスのこと。
サーバーとは、
インターネット上で稼働しているコンピュータで、ブラウザなどから送られてきたリクエストに応じて動作します。
内容調査
怪しい箇所Ⅰ : useState
"2026-06-01-23:59:59"にアクセスした場合のuseStateの挙動を例に説明します。
①ブラウザがリクエスト送信 (ブラウザ側からサイトに"2026-06-01-23:59:59"に初回アクセス)
②サーバーでSSR (サーバーからHTMLを作成)
③HTML受信・表示 (作成したHTMLをブラウザへ反映)
④Hydration (ブラウザ側でReactがイベント処理などの紐付け)
①〜④の流れで処理が行われます。
②ではサーバー側のdateに2026-06-01-23:59:59 が代入されます。
しかし、④のHydration時にはブラウザ側でもコンポーネントが実行されるため、
useState(new Date()) が再度評価されます。
その際に日付が変わっていると、
サーバーで生成したHTMLとブラウザで生成したHTMLの内容が一致せず、
"Hydration Error"が発生します。
補足:
SSRとは、HTMLをサーバーで生成してからブラウザに送る仕組みのこと。
怪しい箇所Ⅱ : react-calendar
react-calendarは内部で現在日時やlocaleに依存した描画を行うため、
SSR環境ではサーバーとブラウザで生成されるHTMLが一致しない場合があります。
別件問題箇所 : カレンダーとDBに保存されている日付の比較
createdAtはUTC基準で扱われます。
そのため、日本時間の2026-06-02 00:30は、
UTCでは2026-06-01 15:30として扱われます。
以下のようなソースコードで比較を行うと、選択した日付で適切なトレーニング記録内容が表示されない問題が発生します。
training.createdAt.getDate() === date.getDate()
対策
対策Ⅰ : useState
dateの初期値をnullにし、
ブラウザでのみ実行されるuseEffect内で現在日時を設定することで、
サーバーとブラウザの初期描画内容の不一致を防ぎます。
なお、useEffectはHydration完了後にブラウザでのみ実行されるため、
SSR時のHTML生成には影響しません。
const [date, setDate] = useState<Date | null>(null); //初期値でnullをセット
useEffect(() => {
const today = new Date();
setDate(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
},[]);
if (!date) return null; //dateが設定されるまで描画しない
対策Ⅱ : react-calendar
react-calendarをSSR対象から外すことで、サーバー側で描画を行わせないようにします。
サーバー側で描画を行わせず、ブラウザ側だけで描画を行うことで"Hydration Error"が発生しないようにしました。
今回の事象についても影響している可能性があったため、
SSR対象から外して検証を行いました。
import dynamic from "next/dynamic";
import "react-calendar/dist/Calendar.css";
const Calendar = dynamic(() => import("react-calendar"), {
ssr: false,
});
別件問題対策 : カレンダーとDBに保存されている日付の比較
createdAtはUTC基準で扱われるため、
日本時間の2026-06-02 00:30は、
UTCでは2026-06-01 15:30として扱われます。
そのため、日本時間の開始時刻と終了時刻をDateで作成し、DB上で範囲検索する方法で対処しました。
const start = new Date("2026-06-01T15:00:00.000Z"); // JST: 2026-06-02 00:00
const end = new Date("2026-06-02T15:00:00.000Z"); // JST: 2026-06-03 00:00
const trainings = await prisma.training.findMany({
where: {
createdAt: {
gte: start,
lt: end,
},
},
});
まとめ
今回のHydration Errorは、new Date() によってサーバー側とブラウザ側で初期表示がずれる可能性があったこと、また react-calendar が内部で日付やlocaleに依存した描画を行うことが原因候補でした。
対策として、初期表示では日付を直接表示せず、useEffect でブラウザ側だけで日付をセットするようにしました。また、react-calendar は dynamic import を使ってSSR対象から外しました。
さらに、DBに保存される createdAt はUTCで扱われるため、カレンダーの日付と単純に getDate() で比較すると、日本時間とのズレが発生する可能性があります。そのため、選択日の開始時刻と終了時刻をUTC基準で作成し、Prismaの gte / lt を使って範囲検索するようにしました。
学んだこと
今回の調査を通して、
Hydration Errorは単純な実装ミスだけでなく、
日時やlocaleのような環境依存の値でも発生することを学びました。
今後はSSRとCSRの違いを意識しながら実装していきたいと思います。