# Real-time updates

React to data changes with `list.onResultChange()` and push changes into dcupl with `data.upsert()`.

## Key dcupl functions

| Function                | Purpose                                                                      |
| ----------------------- | ---------------------------------------------------------------------------- |
| `list.onResultChange()` | Run a callback when the list's result changes (returns unsubscribe function) |
| `dcupl.data.upsert()`   | Add new items or update existing ones (staged)                               |
| `dcupl.data.remove()`   | Remove data items (staged)                                                   |
| `dcupl.update()`        | Apply staged data changes; affected lists emit `result_updated`              |

## Example data

The snippets up to the Complete Flow use this `Product` model:

```typescript title="Setup"
import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl();
dcupl.models.set({
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'stock', type: 'int' },
  ],
});
dcupl.data.set(
  [
    { key: 'prod-1', name: 'Widget', stock: 50 },
    { key: 'prod-2', name: 'Gadget', stock: 100 },
    { key: 'prod-3', name: 'Gizmo', stock: 5 },
  ],
  { model: 'Product' },
);
await dcupl.init();
```

## Listening to list events

Subscribe to result changes on a list with `list.onResultChange()`. It fires once per
`dcupl.update()` that can have changed the list (also for an update with nothing staged), and when
the list's query changes — never for reads such as `items()`:

```typescript
const productList = dcupl.lists.create({ modelKey: 'Product' });

const unsubscribe = productList.onResultChange(() => {
  // Refresh your UI here
  console.log('Products:', productList.catalog.query.items());
});

// Later: cleanup
unsubscribe();
```

`list.on()` receives every event of the list, including reads; filter on
`event.type === 'result_updated'` if you need it. See the [Events reference](/sdk/api/events).

A list created with a `query` option keeps the keys that query selected: after `dcupl.update()` it
serves their current values and removed items drop out, but a new match doesn't join it and an
item that stops matching stays. Call `list.update()` to re-run the query once, or give the list
`autoUpdate: true` (or set `performance.autoUpdateLists.enabled`) to re-run it on each
`dcupl.update()`; the [Auto-Update Pattern](/sdk/api/lists#auto-update-pattern) shows how to pass
it in TypeScript.

## Updating data with data.upsert()

Push new or updated items into dcupl. `upsert()` inserts unknown keys and updates existing items;
every attribute in the payload replaces the stored value, attributes you leave out keep theirs.
Don't use `data.set()` for this: it replaces **all** data of the model.

```typescript
// Add a new item or update an existing one
dcupl.data.upsert([{ key: 'prod-4', name: 'Doohickey', stock: 20 }], { model: 'Product' });

// Apply changes
await dcupl.update();
```

Update multiple items at once:

```typescript
dcupl.data.upsert(
  [
    { key: 'prod-1', stock: 45 },
    { key: 'prod-2', stock: 120 },
    { key: 'prod-3', stock: 0 },
  ],
  { model: 'Product' },
);

await dcupl.update();
```

## Removing data

Remove items by key:

```typescript
dcupl.data.remove([{ key: 'prod-3' }], { model: 'Product' });

await dcupl.update();
```

## Complete flow

Combine event listening with data updates:

```typescript
import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl();
dcupl.models.set({
  key: 'Order',
  properties: [{ key: 'status', type: 'string' }],
});
dcupl.data.set([], { model: 'Order' }); // Start empty
await dcupl.init();

const orderList = dcupl.lists.create({ modelKey: 'Order' });

// 1. Listen for changes
orderList.onResultChange(() => {
  const orders = orderList.catalog.query.items();
  console.log('Orders updated:', orders.length);
});

// 2. Add data (from your data source)
async function handleNewOrder(order: { key: string; status: string }) {
  dcupl.data.upsert([order], { model: 'Order' });
  await dcupl.update(); // orderList emits result_updated
}

// 3. Update existing item
async function updateOrderStatus(orderId: string, status: string) {
  dcupl.data.update([{ key: orderId, status }], { model: 'Order' });
  await dcupl.update();
}

// 4. Remove item
async function deleteOrder(orderId: string) {
  dcupl.data.remove([{ key: orderId }], { model: 'Order' });
  await dcupl.update();
}
```

`data.update()` only changes items that already exist; an unknown key is ignored. Use `upsert()`
when the item may be new.

## Batching updates

For multiple changes, call `dcupl.update()` once at the end:

```typescript
// Queue multiple changes
dcupl.data.upsert([{ key: 'prod-1', stock: 10 }], { model: 'Product' });
dcupl.data.upsert([{ key: 'prod-2', stock: 20 }], { model: 'Product' });
dcupl.data.remove([{ key: 'prod-3' }], { model: 'Product' });

// A single update: each affected list emits result_updated once
await dcupl.update();
```

## Next steps

- [Lists](/sdk/core-concepts/lists) — Creating and managing lists
- [Data management](/sdk/core-concepts/data-management) — Full data operations reference
- [Query results](/sdk/queries/query-results) — Reading data from lists
