RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-04-12Intermediate

Building Offline-First Apps with Rork — Practical Cache Strategies Using AsyncStorage and SQLite

A step-by-step guide to adding offline capabilities to Rork-generated apps, covering data persistence, background sync, and conflict resolution patterns that prevent user churn.

rork58offline-first3asyncstoragesqlitecache-strategy

You open the app on the subway. White screen. The spinner keeps spinning. Five seconds later: "Network Error." The user closes the app and never comes back. One-star reviews pile up on the App Store.

Apps built with Rork often rely on API data for their primary views. But an app that ignores unreliable network conditions is unusable in the real world, no matter how polished its features are. Offline support is not a nice-to-have — it is foundational infrastructure for app survival.

AsyncStorage vs SQLite: Choosing the Right Tool

React Native (the framework underlying Rork's generated code) offers two primary options for data persistence: AsyncStorage and SQLite. Choosing wrong means a painful migration later.

Use AsyncStorage for:

  • User preferences (theme, notification toggles, language)
  • Authentication tokens
  • Last-viewed screen IDs
  • Small JSON payloads under 10KB

Use SQLite for:

  • Structured data like article lists or product catalogs
  • Data that needs searching or filtering
  • Lists exceeding 100 items
  • Data with relational structure

The decision rule is simple: "Will I need to query this data conditionally?" If yes, use SQLite. If no, use AsyncStorage. AsyncStorage is a key-value store and is unsuitable for queries like "items in category X created after date Y."

Basic AsyncStorage caching pattern for a Rork-generated app:

import AsyncStorage from '@react-native-async-storage/async-storage';
 
// Write cache with 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));
};
 
// Read cache with expiry check
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) calibration matters. Too short and data disappears when users go offline; too long and stale content lingers. From experience, news feeds work well at 30 minutes, user profiles at 24 hours, and master data at 7 days.

SQLite for Offline List Screens

List screens with 100+ items need SQLite. Here is how to integrate expo-sqlite into a Rork project:

import * as SQLite from 'expo-sqlite';
 
const db = SQLite.openDatabaseSync('app_cache.db');
 
// Initialize tables (run once at app startup)
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)
  `);
};
 
// Batch cache (API response → 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');
};
 
// Category-filtered retrieval (fast search even offline)
const getArticlesByCategory = (category: string): Article[] => {
  return db.getAllSync<Article>(
    'SELECT * FROM articles WHERE category = ? ORDER BY cached_at DESC',
    category
  );
};

Wrapping bulk inserts in BEGIN TRANSACTION and COMMIT is critical. Inserting 100 rows individually takes around 5 seconds; with a transaction wrapper, it drops below 0.1 seconds. This is a major performance factor for Rork-generated apps.

UI State Management While Offline

Caching data alone is not enough. Users need to know they are viewing cached content:

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 {
        // Network available but API is down
        const cached = await getCachedData<T>(cacheKey);
        if (cached) {
          setData(cached);
          setSource('cache');
        }
      } finally {
        setIsRefreshing(false);
      }
    } else {
      // Offline: read from cache
      const cached = await getCachedData<T>(cacheKey);
      if (cached) {
        setData(cached);
        setSource('cache');
      }
    }
  }, [fetchFn, cacheKey]);
 
  useEffect(() => { loadData(); }, [loadData]);
 
  return { data, source, isRefreshing, refresh: loadData };
};

The source field is the key design element. Use it to show a contextual indicator:

const ArticleList = () => {
  const { data, source, isRefreshing, refresh } = useNetworkAwareData(
    fetchArticles,
    'articles_list'
  );
 
  return (
    <View>
      {source === 'cache' && (
        <View style={styles.offlineBanner}>
          <Text>Offline mode — showing last fetched data</Text>
        </View>
      )}
      <FlatList
        data={data}
        refreshing={isRefreshing}
        onRefresh={refresh}
        renderItem={({ item }) => <ArticleCard article={item} />}
      />
    </View>
  );
};

Optimistic Updates: Writing While Offline

Beyond reads, write operations (likes, bookmarks, comments) should also work offline. This is where the optimistic update pattern comes in:

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); // Keep for 7 days
  };
 
  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 };
};

On the UI side, reflect the user's action immediately and sync in the background:

const handleLike = async (articleId: string) => {
  // 1. Update UI instantly (optimistic)
  setLiked(true);
  setLikeCount(prev => prev + 1);
 
  // 2. Queue the pending action
  await addPendingAction({
    type: 'like',
    payload: { articleId },
  });
};

When the network returns, syncPendingActions sends queued operations to the server. Ideally, use a NetInfo listener to detect reconnection and trigger sync automatically.

Your Next Move

Start by caching data for your most-visited screen using AsyncStorage. That alone eliminates the "white screen on the subway" problem. Offline write support can be introduced incrementally once read caching is stable.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-06-23
The Post You Wrote Offline Shows Up Twice — Designing a Send Outbox That Survives Retries
Persisting a queue and replaying it isn't enough — a lost response turns into a duplicate write. Here's a send outbox with idempotency keys, temp-to-server ID remapping, and poison-message quarantine, in working TypeScript.
Dev Tools2026-06-18
Retrofitting Offline-First Into a Rork App: Persistent Cache and a Write Queue
Reviews kept saying the app was blank on the subway. Polishing error screens was not enough, so I retrofitted TanStack Query persistence and an offline write queue into a Rork-generated Expo app. Optimistic updates, reconnect flushing, and keeping the layer safe from regeneration are all covered with code.
Dev Tools2026-06-15
Running a Neon + Drizzle Backend for Your Rork App in Production — Notes on Edge Connections, Zero-Downtime Migrations, and Type-Safe Queries
After wiring Neon Serverless Postgres and Drizzle ORM into a Rork app's backend, the friction shows up in production. These are implementation notes on choosing an edge connection model, migrating without locking tables, and designing type-safe queries that don't balloon into N+1.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →