Core Concepts
Stores
Stores provide reactive state management for DataSources. They automatically sync with the server, track changes, and update your UI reactively.
What is a Store?
A Store is a valtio-based reactive state container that:
- Wraps DataSource CRUD operations
- Automatically updates UI when data changes
- Tracks dirty state (unsaved changes)
- Handles server synchronization
- Provides query caching and deduplication
Store Identity
Stores are identified by: ${alias}-${datasourceId}-${page}. Stores with the same key are shared across the entire app.
// These return the SAME store instance
const store1 = useStore<Users>({ datasourceId: 'Users', page: 'users-page', alias: 'users-list' });
const store2 = useStore<Users>({ datasourceId: 'Users', page: 'users-page', alias: 'users-list' });
// store1 === store2 ✅
Creating a Store
Always create a custom hook for each store:
// src/app/(secure)/module/feature/hooks/use-store.ts
import { useStore } from '@wayvo-ai/core/client';
import type { Entity } from '@/lib/common/ds/types/module/Entity';
export function useEntityStore() {
return useStore<Entity>({
datasourceId: 'Entity',
page: 'entity-page',
alias: 'entity-list',
limit: 20,
includeCount: true,
autoQuery: true,
sort: { createdAt: -1 },
});
}
Configuration by Use Case
List Page Store
export function useEntityListStore() {
return useStore<Entity>({
datasourceId: 'Entity',
page: 'entity-page',
alias: 'entity-list',
limit: 20,
includeCount: true,
autoQuery: true,
sort: { createdAt: -1 },
});
}
Detail/Edit Page Store
export function useEntityEditStore() {
return useStore<Entity>({
datasourceId: 'Entity',
page: 'entity-detail-page',
alias: 'entity-edit',
limit: 1,
autoQuery: false, // Manual query with ID
});
}
Dialog Form Store
export function useEntityDialogStore() {
return useStore<Entity>({
datasourceId: 'Entity',
page: 'entity-page',
alias: 'entity-dialog', // Separate alias from list
limit: 1,
autoQuery: false,
});
}
Essential Hooks
For Forms (Always use useCurrentRowSync)
import { useCurrentRowSync, useIsStoreDirty, useIsStorePosting } from '@wayvo-ai/core/ui';
function EntityForm({ store }: { store: Store<Entity> }) {
const row = useCurrentRowSync(store); // ✅ Prevents cursor jumping
const isDirty = useIsStoreDirty(store);
const isPosting = useIsStorePosting(store);
if (!row) return null;
return (
<form>
<TextInput
label="Name"
value={row.name || ''}
onChange={(value) => store.setValue('name', value)}
/>
<Button disabled={isPosting || !isDirty}>Save</Button>
</form>
);
}
For binding form fields to the store without manual value/onChange, see Forms – Store-backed form fields.
For Lists (Use useDBRows)
import { useDBRows, useIsStoreLoading } from '@wayvo-ai/core/ui';
function EntityList({ store }: { store: Store<Entity> }) {
const rows = useDBRows(store); // DB rows - fields are guaranteed
const isLoading = useIsStoreLoading(store);
if (isLoading) return <Spinner />;
return (
<ul>
{rows.map((row) => (
<li key={row._id}>{row.name}</li>
))}
</ul>
);
}
CRUD Operations
Create New
// ✅ Initialize BEFORE opening dialog
const handleAdd = useCallback(() => {
store.createNew({
partialRecord: { status: 'draft' },
});
setIsDialogOpen(true);
}, [store]);
Edit Existing
const handleEdit = useCallback((row: Entity) => {
store.setCurrentRow(row);
setIsDialogOpen(true);
}, [store]);
Update Fields
store.setValue('name', 'New Name');
store.setValue('name', 'New Name', rowId);
store.updateRow(rowId, { name: 'New Name', status: 'active' });
Save
// Basic save
const success = await store.save();
// With custom feedback
await store.save({ feedback: 'Entity saved successfully' });
// Suppress all toasts
await store.save({ feedback: 'NONE' });
Important: After save(), the store auto-refreshes. Do NOT call executeQuery() or refresh() after save.
Delete
store.deleteRow(rowId);
await store.save({ feedback: 'Entity deleted' });
Query Data
// Auto-query on mount (via autoQuery: true)
// OR manual query:
await store.executeQuery();
// With filters
await store.executeQuery({
query: {
match: { projectId: 'abc123' },
filters: [{ status: { is: 'active' } }],
sort: { name: 1 },
},
});
// Force refresh (bypass cache)
await store.executeQuery({ force: true });
Row Status
Each row has a _status field:
| Status | Description |
|---|---|
Q | Queried (from database, unchanged) |
I | Insert (new, pending save) |
U | Update (modified, pending save) |
D | Delete (marked for deletion) |
N | New (local-only, won't save) |
Common Hooks Reference
| Hook | Purpose | Returns |
|---|---|---|
useCurrentRowSync | Current row (sync mode) | Row<T> | undefined |
useDBRows | All rows (DB type) | ReadonlyArray<DBRow<T>> |
useRows | All rows (includes new) | ReadonlyArray<Row<T>> |
useRowValue | Field from specific row | T[K] | undefined |
useIsStoreLoading | Loading state | boolean |
useIsStorePosting | Posting/saving state | boolean |
useIsStoreDirty | Has unsaved changes | boolean |
Next Steps
- Server Actions - For complex queries
- Comparisons - How Stores compare to React Query, Prisma, etc.
- Page Pattern - Using stores in pages
- CRUD Operations - Complete CRUD examples