π¦ useSupastashData
The useSupastashData hook is the core way to access and sync data in Supastash.
It handles offline-first fetching, real-time updates, manual refreshes, and fine-grained event control β all built on SQLite + Supabase.
This doc is your all-in-one guide to understanding how to use it properly.
π§ What It Doesβ
useSupastashData is a React hook that:
- Loads data instantly from your local SQLite table (even without internet).
- Keeps that data in sync with Supabase β both ways.
- Subscribes to Supabase realtime changes (
INSERT,UPDATE,DELETE). - Minimizes re-renders via smart batching + memoization.
- Supports filtering, grouping, lazy fetch, and custom callbacks.
It's made to be plug-and-play β but also powerful enough for edge cases.
β Key Featuresβ
- π Offline-first loading from local database
- π Realtime Supabase sync (insert/update/delete)
- π§ Memoized output (no unnecessary renders)
- β―οΈ Lazy mode for manual control
- π€ Custom callbacks for push/pull sync events
- π¦ Batched updates via
flushIntervalMs - π Filter-aware both locally and remotely
- π§© Grouping support with
extraMapKeys
π§ͺ Basic Usageβ
const { data, dataMap } = useSupastashData<Order>("orders");
This loads orders from your local SQLite database, keeps it synced, and returns two things:
dataβ An array of all rowsdataMapβ A map keyed byid
π With Filters, Lazy Load, and Callbacksβ
const { data, dataMap, trigger, cancel, groupedBy } = useSupastashData<Order>(
"orders",
{
shouldFetch: !!userId, // Only fetch if user is available
lazy: true, // Wait for manual trigger
flushIntervalMs: 200, // Reduce render frequency
filter: {
column: "user_id",
operator: "eq",
value: userId,
},
extraMapKeys: ["status", "user_id"],
onInsert: (order) => console.log("Inserted:", order),
onUpdate: (order) => console.log("Updated:", order),
onDelete: (order) => console.log("Deleted:", order),
}
);
useEffect(() => {
trigger(); // Only needed if lazy: true
}, []);
π¦ Return Valuesβ
| Name | Type | Description |
|---|---|---|
data | R[] | Array of rows from local table |
dataMap | Map<string, R> | Keyed map of rows by their id |
groupedBy | Record<string, Map<string, R[]>> | Optional grouped maps by field (if specified) |
trigger | () => void | Starts fetch + subscription (if lazy) |
cancel | () => void | Stops sync and fetch (useful for cleanup) |
βοΈ Hook Optionsβ
| Option | Type | Default | Description |
|---|---|---|---|
shouldFetch | boolean | true | Whether to automatically fetch local data on mount |
lazy | boolean | false | If true, hook does nothing until you manually call trigger() |
extraMapKeys | string[] | β | Build secondary maps for grouping by additional keys (e.g., chat_id) |
filter | RealtimeFilter | β | Filter applied only to Supabase realtime events |
onlyUseFilterForRealtime | boolean | false | If true, filter wonβt affect local queryβjust realtime stream |
sqlFilter | RealtimeFilter[] | β | SQL-style filters applied to local and remote queries |
flushIntervalMs | number | 100 | Debounce interval for UI updates |
realtime | boolean | true | Whether to subscribe to Supabase postgres_changes |
limit | number | 1000 | Max number of local records to load |
daylength | number | β | Fetch only rows created within the last n days |
useFilterWhileSyncing | boolean | true | Apply filter to remote sync as well |
orderBy | string | "created_at" | Column to order results by |
orderDesc | boolean | true | Whether to sort in descending order |
clearCacheOnMount | boolean | false | Clears the shared cache for this table when the hook mounts |
onInsert | (item: any) => void | β | Called when a record is inserted via Supabase |
onUpdate | (item: any) => void | β | Called when a record is updated via Supabase |
onDelete | (item: any) => void | β | Called when a record is deleted via Supabase |
onInsertAndUpdate | (item: any) => void | β | Called for both insert and update events |
onPushToRemote | (items: any[]) => Promise<boolean> | β | Custom push logic for unsynced local records; return true if successful |
extraMapKeys β Fast Lookups & Grouping Built-Inβ
When working with local data like orders, itβs common to filter or group by fields like user_id or status. Doing that with .filter() every time isnβt just repetitive β itβs inefficient, especially as your data grows.
Thatβs where extraMapKeys in useSupatashData comes in.
What It Doesβ
Pass one or more column names to extraMapKeys, and Supastash will automatically generate lookup maps for them:
const { data, dataMap, groupedBy } = useSupatashData("orders", {
extraMapKeys: ["user_id", "status"],
});
Now you get:
dataMap.get("order_123")β fast ID lookupgroupedBy.user_id.get("user_42")β all orders for a usergroupedBy.status.get("pending")β all pending orders
No extra filtering, no extra logic.
Why It Mattersβ
Itβs like having indexed views of your local data β ready to use, already synced, and fast. This is especially useful for rendering dashboards, grouped lists, or filtering data by key fields.
Exampleβ
const userOrders = groupedBy.user_id.get(user.id) ?? [];
One line. No filter(). Faster and scalable.
Use it when you know youβll be accessing your data by specific fields often. It keeps your code cleaner and your UI snappier.
π Advanced Callbacksβ
onInsertAndUpdateβ
This fires on both inserts and updates from Supabase. Perfect for logic like marking messages as "received":
onInsertAndUpdate: async (payload) => {
const { data: local } = await supastash
.from("messages")
.select("*")
.eq("id", payload.id)
.run();
if (!local || local.is_received) return;
await supastash
.from("messages")
.upsert({ ...local, is_received: true })
.run();
};
onPushToRemoteβ
Customize how local records get pushed to Supabase:
onPushToRemote: async (payload) => {
const result = await supabase.from("messages").upsert(payload);
return !result.error;
};
Return true on success β Supastash will retry otherwise.
π Whatβs Nextβ
This hook is the heart of Supastash.