Data Persistence
Constructs use IndexedDB for robust data persistence with optional cloud sync.
useConstructData Hook (Recommended)
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, containsOffline-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.