Patterns

CRUD Operations

Complete patterns for Create, Read, Update, and Delete operations using Stores.

Create

Initialize Store Before Opening Dialog

// ✅ Correct
const handleAdd = useCallback(() => {
  store.createNew({
    partialRecord: { status: 'draft' },
  });
  setIsDialogOpen(true);
}, [store]);

// ❌ Wrong - timing issues
useEffect(() => {
  if (open) {
    store.createNew({ partialRecord: { status: 'draft' } });
  }
}, [open, store]);

Save New Record

const handleSave = async () => {
  const success = await store.save({ feedback: 'Entity created' });
  if (success) {
    onClose();
  }
};

Read

List Page

const store = useStore<Entity>({
  datasourceId: 'Entity',
  page: 'entity-page',
  alias: 'entity-list',
  limit: 20,
  includeCount: true,
  autoQuery: true,  // Auto-fetch on mount
});

// Display in PageLayoutTemplate
<PageLayoutTemplate
  store={store}
  tableColumns={tableColumns}
  // ...
/>

Detail Page

const store = useStore<Entity>({
  datasourceId: 'Entity',
  page: 'entity-detail-page',
  alias: 'entity-edit',
  limit: 1,
  autoQuery: false,
});

useEffect(() => {
  store.executeQuery({
    query: { match: { id } },
    force: true,
  });
}, [id, store]);

const row = useCurrentRow(store);

Update

Edit Existing Record

const handleEdit = useCallback((row: Entity) => {
  store.setCurrentRow(row);
  setIsDialogOpen(true);
}, [store]);

Update Fields

// Update current row
store.setValue('name', 'New Name');

// Update specific row
store.setValue('name', 'New Name', rowId);

// Update multiple fields
store.updateRow(rowId, {
  name: 'New Name',
  status: 'active',
});

Save Changes

const handleSave = async () => {
  const success = await store.save({ feedback: 'Entity updated' });
  if (success) {
    onClose();
  }
};

Delete

Delete with Confirmation

import { confirmWithUser } from '@wayvo-ai/core/ui';

const handleDelete = useCallback(async (entity: Entity) => {
  const confirmed = await confirmWithUser({
    title: 'Delete Entity',
    content: `Are you sure you want to delete "${entity.name}"? This action cannot be undone.`,
  });
  
  if (confirmed) {
    store.deleteRow(entity.id);
    await store.save({ feedback: 'Entity deleted' });
  }
}, [store]);

Delete Without Confirmation

const handleDelete = useCallback(async (entity: Entity) => {
  store.deleteRow(entity.id);
  await store.save({ feedback: 'Entity deleted' });
}, [store]);

Complete Example

'use client';

import { useCallback, useState } from 'react';
import { Popup, useCurrentRowSync, useIsStoreDirty, useIsStorePosting, confirmWithUser } from '@wayvo-ai/core/ui';
import type { Store } from '@wayvo-ai/core/common';
import type { Entity } from '@/lib/common/ds/types/module/Entity';
import { TextInput } from '@wayvo-ai/core/ui';

export function EntityDialog({ open, onOpenChange, store, editingRow }: Props) {
  const row = useCurrentRowSync(store);
  const isDirty = useIsStoreDirty(store);
  const isPosting = useIsStorePosting(store);

  const handleSave = async () => {
    try {
      const success = await store.save();
      if (success) {
        showSuccess(editingRow ? 'Updated' : 'Created');
        onOpenChange(false);
      }
    } catch {
      showError('Failed to save');
    }
  };

  const handleClose = () => {
    store.resetStore();
    onOpenChange(false);
  };

  if (!open) return null;

  return (
    <Popup
      title={editingRow ? 'Edit Entity' : 'Add Entity'}
      onClose={handleClose}
      width={520}
      height={430}
      footer={
        <>
          <Button variant="outline" onClick={handleClose} disabled={isPosting}>
            Cancel
          </Button>
          <Button onClick={handleSave} disabled={isPosting || !isDirty || !row}>
            Save
          </Button>
        </>
      }
    >
      {row && (
        <div className="grid gap-4">
          <TextInput
            label="Name"
            value={row.name || ''}
            onChange={(value) => store.setValue('name', value)}
            required
          />
        </div>
      )}
    </Popup>
  );
}

Important Notes

  1. After save() - Store auto-refreshes, no need to call executeQuery() or refresh()
  2. After deleteRow() - Always call save() to persist the deletion
  3. Dialog initialization - Initialize store BEFORE opening, not in useEffect
  4. Use useCurrentRowSync - For forms to prevent cursor jumping

Next Steps

  • Stores - Learn more about store operations
  • Dialogs - Dialog patterns
Previous
Page Pattern