zustand
π Zustand Store Auto-Hydration via Supastash Eventsβ
Supastash emits automatic refresh events for tables whenever local changes occur β whether from syncing, user updates, or mutations. These events allow you to hydrate your Zustand stores (or any state container) with fresh data without having to manually track when something changed.
You donβt need to call anything β just subscribe to the right event and update your store.
π§ Why This Mattersβ
In local-first apps, keeping your UI in sync with the local database is essential. But polling or manually reloading data on every change is inefficient and error-prone.
Supastash solves this with:
- β Internal refresh tracking
- π Debounced event emissions
- π§΅ Per-table precision
- π Zustand-ready integration
π¨ The Supastash Event Formatβ
Whenever a table changes (via local insert/update/delete), Supastash emits:
const ZUSTAND_PREFIX = "supastash:refreshZustand:";
So for the sales
table, this emits:
"supastash:refreshZustand:sales";
You can subscribe to this with:
supastashEventBus.on("supastash:refreshZustand:sales", hydrateSales);
π¨ How to Hydrate Zustand on Changeβ
Create a reusable hook to automatically subscribe to refresh events and rehydrate stores:
import { useEffect } from "react";
import { supastashEventBus } from "supastash";
const ZUSTAND_PREFIX = "supastash:refreshZustand:";
const HYDRATE_HANDLERS = {
sales: async () => {
await useSalesStore.getState().hydrateSales();
},
customers: async () => {
await useCustomersStore.getState().hydrateCustomers();
},
// Add more table hydration handlers here...
};
export function useHydrateStores() {
useEffect(() => {
for (const [table, handler] of Object.entries(HYDRATE_HANDLERS)) {
const event = `${ZUSTAND_PREFIX}${table}`;
supastashEventBus.on(event, handler);
}
return () => {
for (const [table, handler] of Object.entries(HYDRATE_HANDLERS)) {
const event = `${ZUSTAND_PREFIX}${table}`;
supastashEventBus.off(event, handler);
}
};
}, []);
}
π§ͺ How to Use useHydrateStores
β
Call this hook once at the root of your app (or in a top-level layout/screen) to enable auto-hydration:
export default function AppLayout() {
useHydrateStores(); // π enables all auto-hydration events
return (
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
);
}
You can also scope it to specific screens if needed.
π§ What Does a hydrate
Function Do?β
Each hydrate function is just a data fetch + Zustand update:
import { create } from "zustand";
const prevLimit = 100;
export const useSalesStore = create((set) => ({
sales: [],
hydrateSales: async (newLimit = prevLimit) => {
const { data: sales } = await supastash
.from("sales")
.select("*")
.is("deleted_at", null)
.limit(newLimit)
.run();
if (sales) set({ sales });
},
}));
You can narrow results using
.eq("shop_id", currentShopId)
or.gt("updated_at", lastFetched)
.
βοΈ Too Many Listeners?β
If you're listening to many tables and notice a MaxListenersExceededWarning, you can safely raise the listener limit.
Use configureSupastash()
to increase it:
configureSupastash({
// ...other config
listeners: 500, // increase from default 250
});
This ensures your app doesn't run into limits as more tables or hooks subscribe to refresh events.
β Benefits of This Patternβ
Feature | Benefit |
---|---|
π Auto-refresh | Always fresh data after sync or local change |
π§Ό Centralized hydration | No scattered refresh logic |
β‘ Efficient and debounced | Wonβt over-fetch or re-render too often |
π§© Easy to extend | Add any new table by wiring one hydrateX() |
π€ Works with Zustand | Or any other state system (Valtio, Jotai, etc.) |
β TL;DRβ
- Supastash emits per-table refresh events like
supastash:refreshZustand:sales
- You can hook into them using
supastashEventBus
- Hydrate your Zustand store with fresh local data using
.select().run()
- Everything is debounced, scoped, and efficient by design
- Use
useHydrateStores()
at the top level of your app to enable all hydration events