WorkRunner Documentation

Data Persistence

Constructs use IndexedDB for robust data persistence with optional cloud sync.

The easiest way to persist data in your Construct. Handles IndexedDB storage, versioning, and optional cloud sync automatically.

import { useConstructData } from "@useworkapp/construct-sdk";

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export default function TodoApp() {
  const [todos, setTodos, { status, isLoading }] = useConstructData<Todo[]>(
    "todos",      // key - automatically namespaced
    [],           // default value
    { sync: true } // enable cloud sync (optional)
  );

  if (isLoading) return <div>Loading...</div>;

  const addTodo = (text: string) => {
    setTodos([...todos, { id: crypto.randomUUID(), text, completed: false }]);
  };

  return (
    <div>
      {status === "syncing" && <span>Syncing...</span>}
      {/* Your UI */}
    </div>
  );
}

Hook Return Values

The hook returns a tuple with data, setter, and metadata:

const [data, setData, metadata] = useConstructData<T>(key, defaultValue, options);

// metadata object:
{
  status: 'local' | 'syncing' | 'synced' | 'offline' | 'error',
  isLoading: boolean,
  error: Error | null,
  sync: () => Promise<void>,  // manually trigger sync
  version: number,            // data version for conflict resolution
  lastSyncedAt: Date | null,  // last successful sync time
}

Sync Status Handling

Show users the current sync state for a better experience:

const [data, setData, { status, error }] = useConstructData("items", []);

// Status indicator component
function SyncStatus({ status, error }) {
  switch (status) {
    case "synced":
      return <span className="text-green-500">✓ Saved</span>;
    case "syncing":
      return <span className="text-blue-500">Syncing...</span>;
    case "offline":
      return <span className="text-yellow-500">Offline - changes saved locally</span>;
    case "error":
      return <span className="text-red-500">Sync error: {error?.message}</span>;
    default:
      return null;
  }
}

useDataStore for Complex Data

For Constructs with multiple collections and complex queries, use the data store hook:

import { useDataStore } from "@useworkapp/construct-sdk";

interface Expense {
  id: string;
  amount: number;
  category: string;
  date: string;
}

const [store, { isLoading }] = useDataStore<Expense>({
  name: "expenses",
  schema: {
    id: { type: "string", required: true },
    amount: { type: "number", required: true },
    category: { type: "string", required: true },
    date: { type: "string", required: true },
  },
});

// CRUD operations
await store.create({ id: "1", amount: 50, category: "Food", date: "2024-01-15" });
const expense = await store.read("1");
await store.update("1", { amount: 55 });
await store.delete("1");

// Query with operators
const foodExpenses = await store.query({
  category: { eq: "Food" },
  amount: { gt: 20 },
});

// Available operators: eq, ne, gt, gte, lt, lte, in, contains

Offline-First Pattern

Constructs work offline by default. Use the online status hook to show connection state:

import { useOnlineStatus, useConstructData } from "@useworkapp/construct-sdk";

export default function MyConstruct() {
  const isOnline = useOnlineStatus();
  const [data, setData, { status }] = useConstructData("items", [], { sync: true });

  return (
    <div>
      {!isOnline && (
        <div className="bg-yellow-100 p-2 text-sm">
          You're offline. Changes will sync when you reconnect.
        </div>
      )}
      {/* Your UI */}
    </div>
  );
}

Why IndexedDB?

IndexedDB (Recommended)

  • • 50MB+ storage (browser dependent)
  • • Async, non-blocking
  • • Supports complex queries
  • • Works with cloud sync
  • • Better for large datasets

localStorage (Legacy)

  • • 5MB limit per origin
  • • Synchronous, blocks main thread
  • • String-only storage
  • • No cloud sync support
  • • Simple key-value only

Data Import/Export

Allow users to export and import their data:

import { useImportExport } from "@useworkapp/construct-sdk";

const { exportData, importData } = useImportExport();

// Export to JSON
const handleExport = async () => {
  const blob = await exportData({
    format: "json",  // or "csv"
    stores: ["todos", "settings"],
    compress: true,  // optional gzip compression
  });
  // Download blob...
};

// Import from file
const handleImport = async (file: File) => {
  await importData({
    file,
    onConflict: "replace",  // "skip" | "replace" | "error"
  });
};

Storage Limits

IndexedDB Quotas

IndexedDB storage is much more generous than localStorage:

  • • Chrome/Edge: Up to 60% of disk space
  • • Firefox: Up to 50% of disk space
  • • Safari: ~1GB with user prompts for more
  • • Cloud sync: Based on user's plan tier

Clear Data Option

Always provide users a way to clear their data:

import { clearAllData } from "@useworkapp/construct-sdk";

const handleClearData = async () => {
  if (confirm("Are you sure? This will delete all your data.")) {
    await clearAllData();
    window.location.reload();
  }
};

// In settings
<Button variant="destructive" onClick={handleClearData}>
  Clear All Data
</Button>

Legacy: localStorage Pattern

For simple Constructs or backward compatibility, you can still use localStorage:

const STORAGE_KEY = "work-construct-my-app";

const [data, setData] = useState<MyData>(() => {
  if (typeof window === "undefined") return defaultData;
  const saved = localStorage.getItem(STORAGE_KEY);
  return saved ? JSON.parse(saved) : defaultData;
});

useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}, [data]);

Note: localStorage doesn't support cloud sync. Use useConstructData for new Constructs.

Data Persistence | Runner Documentation | Work