RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-04-29中級

Rork で日付が日本時間にならない・9時間ずれる時の直し方

Rork で作ったアプリで日時が9時間ずれる、日本時間にならない、深夜に日付が変わってしまう——そんな時に確認すべきポイントと、ライブラリ別の安全な書き方を整理しました。

Rork515日付タイムゾーン2JSTトラブルシューティング77React Native209date-fns

家計簿アプリを作っていて「昨日の支出として登録したはずなのに、一覧では今日扱いになっている」。日記アプリで「23時台に書いたメモが翌日の日付で保存されている」。これは私自身、Rork で作ったアプリで何度も踏んだことのある罠です。

原因はだいたい2つに集約されます。サーバーから返ってきた 2026-04-29T00:00:00Z のような UTC 文字列をそのまま new Date() した結果、JavaScript が「9時間前のローカル時刻」として解釈してしまうケース。もう1つは、自分で書いた Date オブジェクトを toISOString() で文字列化したつもりが、UTC に変換されていてサーバー側との認識がずれているケースです。

Rork の AI が生成したコードはそれっぽく動いてしまうため、軽くテストした段階では気付きにくいのが厄介です。今回はこの「9時間ずれ問題」を順番に潰していきます。

まずは「ずれ」の正体を切り分ける

タイムゾーン問題に取り掛かる前に、いま何がどうずれているのかをはっきりさせます。これを飛ばすと、無関係なところを修正してさらに混乱します。

最初にやるのは、画面に出ている値とログ出力を並べて比較することです。

// デバッグ用: 一時的にこれを書いて、各ステップで値を確認する
const raw = '2026-04-29T00:00:00Z';        // サーバーから来た値
const parsed = new Date(raw);
console.log('元の文字列:', raw);
console.log('Date オブジェクト:', parsed);
console.log('ローカル表示:', parsed.toString());
console.log('toISOString():', parsed.toISOString());
console.log('toLocaleString JP:', parsed.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
console.log('getTimezoneOffset:', parsed.getTimezoneOffset());
// 期待: ローカル表示 → 2026/4/29 9:00:00(JST)
//       getTimezoneOffset → -540(JST は UTC より9時間進んでいるので負の値)

これを iOS シミュレータと実機の両方で走らせます。シミュレータは Mac の設定を継ぐので、実機との差で気付くことが結構あります。「実機だけ8時間ずれる」なら端末のタイムゾーン設定が Asia/Shanghai になっていた、というオチも珍しくありません。

パターン1: UTC 文字列をローカル日付として扱ってしまう

一番多いのがこれです。サーバーは 2026-04-29T00:00:00Z(末尾に Z がついた UTC 表記)を返しているのに、クライアント側で getDate()getHours() を呼んでしまうと、JST に変換された後の値が返ります。これは「9時間進んだ値」になるので、深夜帯のレコードで日付が1日ずれます。

// ❌ 日付が変わるバグ
const record = { createdAt: '2026-04-29T00:00:00Z' };  // UTC で2026/04/29 00:00
const d = new Date(record.createdAt);
const dayLabel = `${d.getMonth() + 1}/${d.getDate()}`;  // → 「4/29」と表示したい
// 実際は JST に変換されて 2026/04/29 09:00 になるので → 「4/29」(たまたま合う)
// だが UTC で 2026/04/28T15:00:00Z を投げた場合は JST だと 2026/04/29 00:00
// → クライアントは「4/29」表示、サーバー DB は「4/28」記録、というズレが起きる

サーバー側で「日付の境界」を持つ仕様(家計簿の日次集計、日記の1日1エントリ縛りなど)だと、この種のずれが致命的になります。

解決策は、「日付の意味」を整理して、用途別に書き分けることです。

import { format, parseISO } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';
 
// ✅ 表示用: 必ず JST に変換してからフォーマットする
function formatJstDate(isoString: string): string {
  const utc = parseISO(isoString);                    // UTC として正しく解釈
  const jst = toZonedTime(utc, 'Asia/Tokyo');         // JST に明示変換
  return format(jst, 'yyyy/MM/dd HH:mm');
}
 
// 使用例
const label = formatJstDate('2026-04-29T00:00:00Z');
console.log(label);  // → "2026/04/29 09:00"

date-fns-tztoZonedTime は、内部的には「UTC の数値はそのままに、表示用フィールドだけ JST 相当にずらした Date」を返します。これに format を組み合わせれば、デバイスのタイムゾーン設定に左右されずに日本時間で出せます。

パターン2: ローカル時刻をそのまま JSON で送ってしまう

逆方向の事故もよく起きます。ユーザーが日付ピッカーで「2026年4月29日」を選んだ Date を、JSON.stringify でそのまま送ると、toISOString() が呼ばれて UTC 文字列になります。サーバー側で「文字列の前半10文字を日付とみなす」実装になっていると、ここで日付がずれます。

// ❌ よくある送信コード
const selectedDate = new Date(2026, 3, 29);  // ローカル 2026/04/29 00:00:00 (JST)
const payload = { date: selectedDate };
fetch('/api/diary', { method: 'POST', body: JSON.stringify(payload) });
// 実際に送られる値: { "date": "2026-04-28T15:00:00.000Z" } ← UTC換算で前日になる

これを直すには、「日付として扱いたい値」と「時刻まで含めて扱いたい値」を区別して、前者は文字列で送ります。

import { format } from 'date-fns';
 
// ✅ 日付として扱う場合: YYYY-MM-DD 文字列で送る
const selectedDate = new Date(2026, 3, 29);
const payload = {
  date: format(selectedDate, 'yyyy-MM-dd'),  // → "2026-04-29"
};
fetch('/api/diary', { method: 'POST', body: JSON.stringify(payload) });
 
// ✅ 時刻まで扱う場合: タイムゾーン情報付きの ISO 文字列にする
import { formatISO } from 'date-fns';
const now = new Date();
const payloadWithTime = {
  createdAt: formatISO(now),  // → "2026-04-29T09:30:15+09:00" (タイムゾーンが明示される)
};

タイムゾーン情報を必ず文字列に残しておくのがコツです。これがあれば、後でサーバー側でもクライアント側でも「これは何時何分の出来事か」を一意に再現できます。

パターン3: AsyncStorage に保存した日付が Date ではなく文字列に化ける

ローカル保存系でハマるのがこの罠です。AsyncStorage には文字列しか入らないので、Date を保存して読み戻すと文字列になっています。それを getHours() などで使おうとすると当然エラーで、しかも「たまにエラー」のように見えるので原因究明が遅れます。

import AsyncStorage from '@react-native-async-storage/async-storage';
 
// 保存
const lastOpened = new Date();
await AsyncStorage.setItem('lastOpened', JSON.stringify(lastOpened));
// JSON.stringify が toISOString を呼ぶので、保存される値は "2026-04-29T00:30:15.000Z"
 
// 読み込み — ❌ これだと文字列のまま
const raw = await AsyncStorage.getItem('lastOpened');
const value = raw ? JSON.parse(raw) : null;
console.log(value.getHours());  // TypeError: value.getHours is not a function
 
// ✅ 正しく Date に戻す
const restored = raw ? new Date(JSON.parse(raw)) : null;
console.log(restored?.getHours());  // 正常に動く

この種の問題は AsyncStorage 全般に言えるので、保存処理が絡む別の罠については Rork アプリのデータが消える・保存されない問題の原因と解決策 でも触れています。

パターン4: テストでは合うのに本番で1日ずれる

開発時は問題なかったのに、リリース後にユーザーから「日付がおかしい」とクレームが来るパターンです。原因はだいたい次のどれかです。

  • 開発時は JST 端末で確認しており、海外ユーザーの端末タイムゾーンが想定外だった
  • iOS シミュレータの「Setting Date & Time」がデフォルトの UTC のままだった
  • サマータイム(DST)のある国で、変わり目をまたぐデータが入ってきた

これを未然に防ぐには、開発中から「異なるタイムゾーンの動作」を試す癖をつけます。Mac であれば、ターミナルから一時的にタイムゾーンを変えてシミュレータを起動できます。

# Mac のシミュレータを別タイムゾーンで動かす(例: ハワイ)
TZ='Pacific/Honolulu' npx expo run:ios
 
# 終わったら通常通りに戻す(再起動でも戻る)
unset TZ

私はリリース前に必ず最低3つのタイムゾーン(JST・UTC・PDT)でアプリを動かすようにしています。日付の境界をまたぐテストデータを入れて、表示が崩れないかを目視で確かめます。

パターン5: new Date('2026-04-29') が iOS と Android で違う動きをする

これは細かい仕様差ですが、引っかかるとデバッグに半日かかります。new Date('2026-04-29') のような「ハイフン区切りの日付文字列」を渡したとき、iOS の Hermes と Android の Hermes で UTC として扱うか、ローカル時刻として扱うかが微妙に違うことがあります。

回避策はシンプルで、自前でパースするライブラリを必ず通すことです。

// ❌ 環境差が出る可能性あり
const d1 = new Date('2026-04-29');         // 環境によって挙動が違う
const d2 = new Date('2026/04/29');         // iOS で NaN になることがある
 
// ✅ date-fns の parse / parseISO を使う
import { parse, parseISO } from 'date-fns';
const d3 = parseISO('2026-04-29');                          // ISO 8601 専用
const d4 = parse('2026/04/29', 'yyyy/MM/dd', new Date());   // 任意フォーマット

parseISO は ISO 8601 限定で安全に、parse は任意のフォーマットを明示的に指定できます。new Date() の文字列パースに頼らないことが、クロスプラットフォーム開発では事故防止になります。

ライブラリ選びの個人的な指針

私はもう moment.js は使わなくなって、date-fns + date-fns-tz の組み合わせに落ち着いています。理由は3つあります。

  • 関数ベースなので、必要なものだけ import すればバンドルが小さくなる
  • イミュータブルなので、知らないうちに元の Date が書き換わる事故がない
  • TypeScript の型サポートが手厚い

軽量さで dayjs を選ぶ人も多いですが、タイムゾーンプラグインを毎回 import するのが面倒で、私は date-fns のほうが使いやすく感じます。Rork で AI に「日付処理を書いて」と頼むと moment を持ってこようとすることがあるので、そのときは「date-fns を使って」と明示するか、生成後に置き換えるとよいです。

日付・タイムゾーン関連の落とし穴も実例ベースで解説されていて、Rork で生成したコードのレビューにも役立ちます。

全体を振り返って:今日できる予防策を1つだけ

長く書きましたが、もし今すぐできることを1つだけ選ぶなら、プロジェクトに formatJstDate のような薄いラッパー関数を1つ用意して、画面表示はすべてそれを通すことです。

// utils/datetime.ts として配置
import { format, parseISO } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';
 
export function formatJstDate(input: string | Date, pattern = 'yyyy/MM/dd HH:mm'): string {
  const utc = typeof input === 'string' ? parseISO(input) : input;
  const jst = toZonedTime(utc, 'Asia/Tokyo');
  return format(jst, pattern);
}

これがあるだけで、新しい画面を作るたびに「あれ、タイムゾーンどう書くんだっけ」と迷うことがなくなります。Rork の AI も、このユーティリティが既に存在することを認識すれば、生成コードの中で自然に呼び出してくれるようになります。

通信周りで詰まったときは Rork アプリの状態がうまく更新されない・再描画されない時の対処法 も参考になるかもしれません。日付処理のバグはときどき再描画タイミングと絡み合って症状を変えるので、合わせて見ておくと原因の切り分けが早くなります。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-06-25
海外に行くと毎朝の通知が9時に来ない — Expoの定時ローカル通知をタイムゾーンと夏時間に強くする設計
Rork(Expo)で作った定時リマインダーが、出張やサマータイムで現地の9時に届かなくなる問題。timeIntervalトリガーの落とし穴と、ローカル時刻基準で再スケジュールする設計を実装コード付きで整理します。
開発ツール2026-06-12
Rork 製アプリの『書類とデータ』が数 GB に膨らむときの原因と対処 — expo-image のディスクキャッシュ管理
アプリ本体は小さいのに『書類とデータ』だけが数 GB に膨らむ——壁紙アプリの運用で実際に踏んだ expo-image のディスクキャッシュ肥大を、cachePolicy の使い分け・縮小版配信・世代別クリアの三段構えで解決した記録です。
開発ツール2026-05-25
Rork で targetSdkVersion 35 にしたら Android のレイアウトが下まで埋まる問題の直し方
Android 15 (API 35) で強制されるエッジ・トゥ・エッジ表示によって、Rork で生成したアプリの下部タブやヘッダーがシステムバーに食い込む問題を、react-native-edge-to-edge と useSafeAreaInsets を使って実装パターン別に解決します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →