日付を扱うときはSSRとCSRの違いに注意
開発環境
- Next.js 16.2.1(App Router)
- date-fns 4.1.0
前提
Next.js の App Router では、コンポーネントはデフォルトで SSR(サーバーサイドレンダリング)で動作します。
"use client" を付けることで CSR(クライアントサイドレンダリング)に切り替えられます。
本題
事象
日時を表示するコンポーネントで、時刻が UTC で表示されていました。
日本時間(JST)で表示してほしいのに、9時間ずれた時刻が出てきます。
// (修正前)
import { format } from 'date-fns'
const SomeDetail = ({ item }) => {
return (
<div>
<p>{format(new Date(item.createdAt), 'yyyy/MM/dd HH:mm')}</p>
</div>
)
}
export default SomeDetailなぜ UTC で表示されるのか
date-fns の format() は実行環境のタイムゾーンを参照します。
- SSR(サーバー):サーバーのタイムゾーン(多くの場合 UTC)
- CSR(ブラウザ):ユーザーのブラウザのタイムゾーン(日本なら JST)
App Router のデフォルトはサーバーコンポーネントです。 "use client" を書き忘れると、format() がサーバーで実行されてしまいます。
⚠️ 「日付処理なのに時刻がずれる」と感じたら、まず実行環境を疑う。
修正
"use client" を追加して CSR に変更します。
// (修正後)
'use client'
import { format } from 'date-fns'
const SomeDetail = ({ item }) => {
return (
<div>
<p>{format(new Date(item.createdAt), 'yyyy/MM/dd HH:mm')}</p>
</div>
)
}
export default SomeDetail👉 これだけで format() がブラウザ上で実行されるようになり、ユーザーのローカルタイムゾーン(JST)が適用されます。
別の解決アプローチ
CSR に切り替える以外にも対処法はあります。
- @date-fns/tz を使ってタイムゾーンを明示指定する
- Intl.DateTimeFormat に timeZone オプションを渡す
今回のケースは「ユーザーのローカル時刻で表示したい」という要件だったため、"use client" による CSR 化がシンプルな解決策でした。
タイムゾーンを固定したい場面(例:日本時間で統一したいが、SSR のまま使いたい)では @date-fns/tz の formatInTimeZone() が有効です。
さいごに
SSR と CSR でタイムゾーンの扱いが変わるという落とし穴は、意識していないと踏みやすいです。
date-fns に限らず、実行環境のタイムゾーンに依存する処理は「どこで実行されるか」を意識することが大切です。
Next.js App Router を使うなら「デフォルトはサーバー」という前提を常に頭に置いておきたい。