.md

Lists

Filtered views of your data - create, query, and manage independent data views.

What is a list?

A DcuplList is a filtered view of your model data. Think of it as a live, queryable dataset: filter changes apply immediately, and data changes show up after dcupl.update().

Each list:

  • Represents a specific model's data
  • Provides a query interface (via catalog)
  • Can have filters applied to it
  • Recomputes its result when filters change or dcupl.update() applies data changes
  • Manages its own state independently
create-list.ts
const productList = dcupl.lists.create({
  modelKey: 'Product'
});

// Access items through the catalog
const items = productList.catalog.query.items();

Why lists?

Multiple views of same data

Create multiple lists for the same model, each with different filters. Great for dashboards, comparison views, or different user contexts.

Independent state

Each list maintains its own query filters, sort order, and cache. Filtering one list doesn't affect others.

Scoped lists

A list created with a query only contains the items that match it. Its own filters start empty and work on top of that subset.

multiple-lists.ts
// Multiple views of the same Product model
const allProducts = dcupl.lists.create({
  modelKey: 'Product'
});

// Scoped list: only contains Electronics products
const electronics = dcupl.lists.create({
  modelKey: 'Product',
  query: {
    modelKey: 'Product',
    queries: [
      { attribute: 'category', operator: 'eq', value: 'Electronics' }
    ],
  },
});

const inStock = dcupl.lists.create({
  modelKey: 'Product',
  query: {
    modelKey: 'Product',
    queries: [
      { attribute: 'inStock', operator: 'eq', value: true }
    ],
  },
});

Creating lists

Lists can be created with various options:

Option Description
modelKey Required. The model to create a view of
listKey Named identifier for later retrieval
query Seed query. Makes a scoped list: it contains only the items that match the query when the list is created. The list's own filters start empty, and clear() does not remove the scope.
listItemKeys Scope the list to these item keys instead of a query. It always serves the current items for exactly these keys, with or without autoUpdate
autoUpdate Lists with a seed query only: re-run it on every dcupl.update() that affects the list, so new matches join and items that stop matching leave. A listItemKeys list serves the same items either way (with it, the list also emits list_updated on every update that affects it)

A list created with listItemKeys or query is scoped: it keeps a set of keys (the listItemKeys you pass, or the keys the seed query matches when the list is created) and always serves the model's current items for those keys. After every dcupl.update(), also after a full rebuild (data.set(), data.reset() or a large update), changed values show, removed items drop out, and an item added under one of those keys is served. For a listItemKeys list that is all there is, so autoUpdate changes only the events, not the items. Items that start or stop matching a seed query join or leave only when you call list.update() or enable auto-update. Lists without a scope see every data change after dcupl.update(): affected lists drop their cached results and recompute on the next read. Set auto-update for all lists with new Dcupl({ performance: { autoUpdateLists: { enabled: true } } }), or per call with dcupl.update({ autoUpdateLists: true }). The full behaviour is in create().

autoUpdate on lists.create() works at runtime, but the TypeScript type for create() does not list it.

creating-lists.ts
// Basic creation
const allProducts = dcupl.lists.create({ modelKey: 'Product' });

// With named key for later retrieval
const featured = dcupl.lists.create({
  modelKey: 'Product',
  listKey: 'featured-products',
});
const sameList = dcupl.lists.get('featured-products');

// Scoped list: only items matching the seed query
const electronics = dcupl.lists.create({
  modelKey: 'Product',
  query: {
    modelKey: 'Product',
    queries: [
      { attribute: 'category', operator: 'eq', value: 'Electronics' }
    ],
  },
});

// Scoped list from item keys
const picked = dcupl.lists.create({
  modelKey: 'Product',
  listItemKeys: ['p1', 'p2'],
});

// A new Electronics product...
dcupl.data.upsert(
  [{ key: 'p9', name: 'Phone', category: 'Electronics' }],
  { model: 'Product' }
);
await dcupl.update();
// ...shows up in allProducts right away. To add it to the
// scoped electronics list, re-run its seed query:
electronics.update();
// (or apply the update with dcupl.update({ autoUpdateLists: true }))

Subscribe to changes

Lists emit events when their state changes. Subscribe to react to these changes.

When to update the UI

Use list.onResultChange(). It fires when the list's result can have changed:

  • Filter changes - addCondition, addGroup, clear, ...
  • Data changes - once per dcupl.update() that affects the list

Reads such as items() or fn.facets() never trigger it. It returns a function that removes the subscription.

All events

list.on() receives every message of the list, including reads. Each message has a type, for example:

Type When
result_updated The result can have changed (what onResultChange listens to)
query_apply, query_remove, query_reset The list's query changed
query_execute, fn_facets, ... A read ran
list_updated list.update() was called
subscribe-changes.ts
const list = dcupl.lists.create({ modelKey: 'Product' });

// renderProducts: your own render function
const unsubscribe = list.onResultChange(() => {
  renderProducts(list.catalog.query.items());
});

// Later: cleanup
unsubscribe();

// Every event of the list
const off = list.on((event) => {
  console.log(event.type, event.listKey);
});

// React example (setProducts from useState)
useEffect(() => {
  const list = dcupl.lists.create({ modelKey: 'Product' });

  const unsubscribe = list.onResultChange(() => {
    setProducts(list.catalog.query.items());
  });

  // Set initial data
  setProducts(list.catalog.query.items());

  return () => {
    unsubscribe();
    list.destroy();
  };
}, []);

List lifecycle

A list's life is: create (lists.create()) → query (filters and reads, any number of times) → data updates (each dcupl.update() refreshes the affected lists) → destroy (list.destroy()). A list has no state property to check; it is usable from create() until destroy().

Always destroy lists when done to prevent memory leaks.

lifecycle.ts
// Create
const list = dcupl.lists.create({ modelKey: 'Product' });

// Use - apply filters
list.catalog.query.addCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics'
});

// Get results
const items = list.catalog.query.items();

// Apply data changes - the list's next read sees them
dcupl.data.upsert([{ key: 'p9', name: 'Tablet', category: 'Electronics' }], { model: 'Product' });
await dcupl.update();

// Destroy when done
list.destroy();

Managing lists

dcupl provides methods to manage all your lists:

  • get(key) - Retrieve a list by its key
  • getAll() - Get all lists
  • getAll({ modelKey }) - Get lists for a specific model
  • destroy({ listKey }) - Destroy a specific list
  • destroy({ modelKey }) - Destroy all lists of a model; destroy() destroys every list
managing-lists.ts
// Get list by key
const list = dcupl.lists.get('my-list');

// Get all lists
const allLists = dcupl.lists.getAll();

// Get lists for a specific model
const productLists = dcupl.lists.getAll({ modelKey: 'Product' });

// Destroy specific list
dcupl.lists.destroy({ listKey: 'my-list' });

// Or destroy via list instance
list.destroy();

Best practices

Use named lists for retrieval

Give lists a listKey so you can retrieve them anywhere in your app without passing references.

Destroy lists in component cleanup

Always destroy lists when components unmount to prevent memory leaks.

Reuse lists instead of creating new ones

Change filters on an existing list rather than creating new lists for each filter change.

best-practices.ts
// Named lists for easy retrieval
const mainList = dcupl.lists.create({
  modelKey: 'Product',
  listKey: 'main-product-list',
});
// Retrieve anywhere
const sameList = dcupl.lists.get('main-product-list');

// Cleanup in React
useEffect(() => {
  const list = dcupl.lists.create({ modelKey: 'Product' });
  return () => list.destroy();
}, []);

// Reuse lists - change filters, don't recreate
const filterList = dcupl.lists.create({ modelKey: 'Product' });

function filterByCategory(category: string) {
  filterList.catalog.query.clear();
  filterList.catalog.query.addCondition({
    attribute: 'category',
    operator: 'eq',
    value: category
  });
  return filterList.catalog.query.items();
}