π¦ 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 | Fetch on mount? |
lazy | boolean | false | If true, nothing runs until you call trigger() |
extraMapKeys | string[] | β | Build grouped maps (e.g., groupedBy.chat_id ) |
filter | RealtimeFilter | β | Only sync matching rows (eq , lt , etc.) |
flushIntervalMs | number | 100 | Debounce for batched UI updates |
realtime | boolean | true | Subscribe to Supabase postgres_changes |
limit | number | 200 | Limit for local rows |
daylength | number | β | Fetch only records from the last n days |
useFilterWhileSyncing | boolean | true | Use same filter when syncing with Supabase |
onInsert | (item: any) => void | β | Called on Supabase INSERT |
onUpdate | (item: any) => void | β | Called on Supabase UPDATE |
onDelete | (item: any) => void | β | Called on Supabase DELETE |
onInsertAndUpdate | (item: any) => void | β | Shortcut for insert + update |
onPushToRemote | (items: any[]) => Promise<boolean> | β | Handle custom push logic (must return success boolean) |
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.
π Manual Refreshβ
Need to manually trigger a refresh? Use these from:
import {
refreshTable,
refreshAllTables,
} from "supastash/utils/sync/refreshTables";
π refreshTable(table: string): void
β
For refreshing one table's local data:
refreshTable("orders");
Emits a refresh:orders
event. Re-fetches local rows + triggers UI update.
π refreshAllTables(): void
β
Refreshes everything:
refreshAllTables();
Good after a full sync or reset.
β οΈ refreshTableWithPayload()
(Deprecated)β
This used to manually reflect data in the UI. No longer needed β use Supastash queries or refreshTable()
.
// Deprecated β avoid
refreshTableWithPayload("orders", { id: "abc", ... }, "update");
π Behind the Scenesβ
- π Supastash uses a version-based cache: if nothing changes, no re-renders.
- π§Ό Realtime listeners are de-duped per
table + filter
. - β‘ Flushes UI updates only after debounce (default 100ms).
- π§ Grouped maps (via
extraMapKeys
) useMap<string, R[]>
.
π‘ Best Practicesβ
- Use
dataMap
for instant lookups (e.g.,dataMap.get(id)
) - Use
groupedBy.chat_id
for message grouping - Use
lazy: true
in modal screens or deeply nested routes - Bump
flushIntervalMs
higher if syncing high-volume tables - Leverage
onPushToRemote
for custom API pipelines
π Whatβs Nextβ
This hook is the heart of Supastash.