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:

StatusDescription
QQueried (from database, unchanged)
IInsert (new, pending save)
UUpdate (modified, pending save)
DDelete (marked for deletion)
NNew (local-only, won't save)

Common Hooks Reference

HookPurposeReturns
useCurrentRowSyncCurrent row (sync mode)Row<T> | undefined
useDBRowsAll rows (DB type)ReadonlyArray<DBRow<T>>
useRowsAll rows (includes new)ReadonlyArray<Row<T>>
useRowValueField from specific rowT[K] | undefined
useIsStoreLoadingLoading stateboolean
useIsStorePostingPosting/saving stateboolean
useIsStoreDirtyHas unsaved changesboolean

Next Steps

Previous
DataSources