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-12中級

Rork でオフライン対応アプリを構築する — AsyncStorage と SQLite を使ったキャッシュ戦略の実践

Rork で生成したアプリにオフライン機能を実装する方法を、データの永続化からバックグラウンド同期まで段階的に解説。ユーザー離脱を防ぐキャッシュ設計の実践パターンを紹介します。

rork58offline-first2asyncstoragesqlitecache-strategy

地下鉄の中でアプリを開いたら真っ白な画面。ローディングスピナーが回り続けて、5秒後に「ネットワークエラー」——ユーザーはアプリを閉じ、二度と戻ってこありません。App Store のレビューには星1つが並ぶ。

Rork で作ったアプリは API から取得したデータを表示するものが多いです。だが、ネットワーク接続が不安定な環境を考慮していないアプリは、どれだけ機能が優れていても実用に耐えありません。オフライン対応は「あると嬉しい機能」ではなく、アプリの生存に関わる基盤設計です。

AsyncStorage と SQLite: 何をどちらに入れるか

React Native(Rork の出力するコードの基盤)でデータを永続化する方法は主に2つあります。AsyncStorage と SQLite です。両者の選択を誤ると、後から移行するのは非常に困難になります。

AsyncStorage を使うべきデータ:

  • ユーザー設定(テーマ、通知のON/OFF、言語設定)
  • 認証トークン
  • 最後に表示した画面のID
  • 10KB以下の小さなJSON

SQLite を使うべきデータ:

  • 記事一覧、商品カタログなどの構造化データ
  • 検索やフィルタリングが必要なデータ
  • 100件以上のリストデータ
  • リレーションを持つデータ

判断基準はシンプルです。「このデータを条件付きで取得したいか?」——YES なら SQLite、NO なら AsyncStorage。AsyncStorage は key-value ストアであり、「カテゴリがXで、日付がY以降のデータ」のような条件検索には向かありません。

Rork で生成されたアプリに AsyncStorage を導入する基本パターン:

import AsyncStorage from '@react-native-async-storage/async-storage';
 
// キャッシュの書き込み(TTL付き)
const cacheData = async (key: string, data: unknown, ttlMinutes: number = 60) => {
  const cacheEntry = {
    data,
    timestamp: Date.now(),
    ttl: ttlMinutes * 60 * 1000,
  };
  await AsyncStorage.setItem(key, JSON.stringify(cacheEntry));
};
 
// キャッシュの読み取り(期限切れチェック付き)
const getCachedData = async <T>(key: string): Promise<T | null> => {
  const raw = await AsyncStorage.getItem(key);
  if (\!raw) return null;
 
  const entry = JSON.parse(raw);
  const isExpired = Date.now() - entry.timestamp > entry.ttl;
 
  if (isExpired) {
    await AsyncStorage.removeItem(key);
    return null;
  }
 
  return entry.data as T;
};

このパターンで注意すべきは TTL(Time To Live)の設定です。短すぎるとオフライン時にデータが消えてしまい、長すぎると古い情報が表示され続ける。私の経験では、ニュースフィード系は30分、ユーザープロフィール系は24時間、マスターデータ系は7日間が良いバランスです。

SQLite でリスト画面のオフライン対応を実現する

100件以上のデータを扱うリスト画面では、SQLite が必要になります。Rork のプロジェクトに expo-sqlite を組み込む手順:

import * as SQLite from 'expo-sqlite';
 
const db = SQLite.openDatabaseSync('app_cache.db');
 
// テーブル初期化(アプリ起動時に1回実行)
const initDatabase = () => {
  db.execSync(`
    CREATE TABLE IF NOT EXISTS articles (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      category TEXT,
      content TEXT,
      cached_at INTEGER,
      is_synced INTEGER DEFAULT 1
    )
  `);
  db.execSync(`
    CREATE INDEX IF NOT EXISTS idx_articles_category 
    ON articles(category)
  `);
};
 
// データの一括キャッシュ(API レスポンス → SQLite)
const cacheArticles = (articles: Article[]) => {
  const now = Date.now();
  const stmt = db.prepareSync(
    'INSERT OR REPLACE INTO articles (id, title, category, content, cached_at) VALUES (?, ?, ?, ?, ?)'
  );
 
  db.execSync('BEGIN TRANSACTION');
  for (const article of articles) {
    stmt.executeSync(article.id, article.title, article.category, article.content, now);
  }
  db.execSync('COMMIT');
};
 
// カテゴリ別取得(オフラインでも高速検索可能)
const getArticlesByCategory = (category: string): Article[] => {
  return db.getAllSync<Article>(
    'SELECT * FROM articles WHERE category = ? ORDER BY cached_at DESC',
    category
  );
};

BEGIN TRANSACTIONCOMMIT で一括挿入をラップしているのは重要なポイントです。100件のデータを個別にINSERTすると5秒かかるところが、トランザクションで囲むと0.1秒以下になります。Rork で生成したアプリのパフォーマンスに大きく影響する部分です。

オフライン時の UI 状態管理

データのキャッシュだけでは十分ではありません。ユーザーに「今オフラインだけどデータは見られる」と伝える UI も必要です:

import NetInfo from '@react-native-community/netinfo';
import { useEffect, useState, useCallback } from 'react';
 
const useNetworkAwareData = <T>(
  fetchFn: () => Promise<T>,
  cacheKey: string
) => {
  const [data, setData] = useState<T | null>(null);
  const [source, setSource] = useState<'network' | 'cache' | 'none'>('none');
  const [isRefreshing, setIsRefreshing] = useState(false);
 
  const loadData = useCallback(async () => {
    const netState = await NetInfo.fetch();
 
    if (netState.isConnected) {
      try {
        setIsRefreshing(true);
        const freshData = await fetchFn();
        setData(freshData);
        setSource('network');
        await cacheData(cacheKey, freshData);
      } catch {
        // ネットワーク接続はあるが API が落ちている場合
        const cached = await getCachedData<T>(cacheKey);
        if (cached) {
          setData(cached);
          setSource('cache');
        }
      } finally {
        setIsRefreshing(false);
      }
    } else {
      // オフライン: キャッシュから読み取り
      const cached = await getCachedData<T>(cacheKey);
      if (cached) {
        setData(cached);
        setSource('cache');
      }
    }
  }, [fetchFn, cacheKey]);
 
  useEffect(() => { loadData(); }, [loadData]);
 
  return { data, source, isRefreshing, refresh: loadData };
};

source フィールドを返しているのがポイントです。これを使って UI にインジケーターを表示できる:

const ArticleList = () => {
  const { data, source, isRefreshing, refresh } = useNetworkAwareData(
    fetchArticles,
    'articles_list'
  );
 
  return (
    <View>
      {source === 'cache' && (
        <View style={styles.offlineBanner}>
          <Text>オフラインモード — 最後に取得したデータを表示中</Text>
        </View>
      )}
      <FlatList
        data={data}
        refreshing={isRefreshing}
        onRefresh={refresh}
        renderItem={({ item }) => <ArticleCard article={item} />}
      />
    </View>
  );
};

オプティミスティック更新: オフラインでの書き込み

読み取りだけでなく、書き込み操作(いいね、ブックマーク、コメントなど)もオフラインで対応したい。ここで使うのがオプティミスティック更新パターンです:

const usePendingActions = () => {
  const addPendingAction = async (action: PendingAction) => {
    const pending = await getCachedData<PendingAction[]>('pending_actions') || [];
    pending.push({ ...action, createdAt: Date.now() });
    await cacheData('pending_actions', pending, 7 * 24 * 60); // 7日間保持
  };
 
  const syncPendingActions = async () => {
    const pending = await getCachedData<PendingAction[]>('pending_actions') || [];
    const failed: PendingAction[] = [];
 
    for (const action of pending) {
      try {
        await executeAction(action);
      } catch {
        failed.push(action);
      }
    }
 
    await cacheData('pending_actions', failed, 7 * 24 * 60);
    return { synced: pending.length - failed.length, failed: failed.length };
  };
 
  return { addPendingAction, syncPendingActions };
};

UI側では、ユーザーの操作を即座に反映し、バックグラウンドで同期する:

const handleLike = async (articleId: string) => {
  // 1. UI を即座に更新(楽観的)
  setLiked(true);
  setLikeCount(prev => prev + 1);
 
  // 2. ペンディングアクションに追加
  await addPendingAction({
    type: 'like',
    payload: { articleId },
  });
};

ネットワークが復帰したとき、syncPendingActions を実行すれば溜まった操作がサーバーに送信されます。NetInfo のリスナーでネットワーク復帰を検知して自動同期するのが理想的です。

次のステップ

まずは最もよく使われる画面のデータを AsyncStorage でキャッシュすることから始めてほしい。それだけで「地下鉄で開いたら真っ白」問題は解消します。書き込みのオフライン対応は、読み取りキャッシュが安定してから段階的に導入すれば十分です。

シェア

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

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

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

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

関連記事

開発ツール2026-06-23
オフラインで書いた投稿が、復帰した瞬間にもう一つ増える——再送に強い送信アウトボックスの設計
永続キューで再送するだけでは、失われた応答が二重投稿を生みます。冪等キー・一時IDの付け替え・毒メッセージの隔離を備えた送信アウトボックスを、動くTypeScriptで設計します。
開発ツール2026-06-15
Rork アプリの Neon × Drizzle バックエンドを本番で運用する — エッジ接続・無停止マイグレーション・型安全クエリの実装メモ
Rork で生成したアプリのバックエンドに Neon Serverless Postgres と Drizzle ORM を据えたあと、本番で詰まる箇所を実装ベースで整理しました。エッジでの接続モデルの選択、無停止マイグレーション、型安全クエリの設計までをまとめています。
開発ツール2026-06-09
Rork Max でディープリンクを実装する — Universal Links と URL Schemes
Rork Max アプリにディープリンクを追加する実装ガイド。URL Scheme と Universal Links の使い分け、AASA/assetlinks の落とし穴、コールド起動の取りこぼしを実例つきで解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →