Reference

Collection Queries (Query)

where/select/orderBy/groupBy chains with Query.from, the methods that produce a result, and use with reactivity.

Query<T> is a small wrapper for querying an array, or any iterable source, with chained steps. Filtering, transforming, sorting and grouping steps are written readably in a single chain:

import { Query } from "@motifx/core";

const titles = Query.from(notes.state.items)
  .where((note) => !note.archived)
  .orderBy((note) => note.title)
  .select((note) => note.title)
  .toArray();
import { Query } from "@motifx/core";

const titles = Query.from(notes.state.items)
  .where((note) => !note.archived)
  .orderBy((note) => note.title)
  .select((note) => note.title)
  .toArray();

Query is optional: it goes into the bundle only when you import it, and it changes no global object, Array.prototype included. A query always starts with Query.from(...).

Creating a query

Form Description
Query.from(source) Produces a query from the source.
new Query(source) The same as Query.from.

source can be an array or any Iterable<T>: a Set, Map.keys(), Map.values(), generator functions, reactive arrays.

Query.from(new Set(["notes", "pinned"]));
Query.from(drafts.values());
Query.from(notes.state.items);          // a reactive array
Query.from(new Set(["notes", "pinned"]));
Query.from(drafts.values());
Query.from(notes.state.items);          // a reactive array

If the source is an array, it is not copied but read directly; other iterables are copied into an array at the moment of Query.from. The source array is not changed by any step.

Chained steps

These methods return a new Query; another step can be added after them.

Method Description
where(predicate) Keeps the items that satisfy the condition. predicate(item, index).
select(selector) Transforms each item. selector(item, index).
orderBy(keySelector) Sorts ascending by key.
orderByDescending(keySelector) Sorts descending by key.
distinctBy(keySelector) Keeps only the first of the items that share a key.
groupBy(keySelector) Splits the items into { key, items } groups; the result is a Query<Group<TKey, T>>.

Methods that produce a result

These methods end the chain and return a value.

Method Returns Description
toArray() T[] Copies the result into a new array.
first() T The first item; throws a MotifError (MJX601) if the query is empty.
firstOrDefault() T \| undefined The first item; undefined if the query is empty.
any(predicate?) boolean Without a condition, "is there at least one item"; with one, "is there an item that satisfies the condition".
all(predicate) boolean Whether every item satisfies the condition.
aggregate(seed, func) TResult Reduces the items to a single value, starting from the seed value.

Query is iterable, so it can be walked directly without calling toArray():

for (const title of Query.from(notes.state.items).select((note) => note.title)) {
  console.log(title);
}

const titles = [...Query.from(notes.state.items).select((note) => note.title)];
for (const title of Query.from(notes.state.items).select((note) => note.title)) {
  console.log(title);
}

const titles = [...Query.from(notes.state.items).select((note) => note.title)];

Examples

Sorting

const latest = Query.from(notes.state.items)
  .orderByDescending((note) => note.updatedAt)
  .toArray();
const latest = Query.from(notes.state.items)
  .orderByDescending((note) => note.updatedAt)
  .toArray();

The sort is stable: items with equal keys keep their order from the source. Keys are compared with < and >; number, string and Date keys work as expected. Strings are sorted by character code; if you need a language's alphabetical order (Turkish, for example, where ç and ş belong right after c and s), sort the result yourself:

const byTitle = Query.from(notes.state.items).toArray().sort((a, b) => a.title.localeCompare(b.title, "tr"));
const byTitle = Query.from(notes.state.items).toArray().sort((a, b) => a.title.localeCompare(b.title, "tr"));

Grouping

const perDay = Query.from(notes.state.items)
  .groupBy((note) => new Date(note.updatedAt).toDateString())
  .select((group) => ({ day: group.key, count: group.items.length }))
  .toArray();
const perDay = Query.from(notes.state.items)
  .groupBy((note) => new Date(note.updatedAt).toDateString())
  .select((group) => ({ day: group.key, count: group.items.length }))
  .toArray();

The groups come in the order in which their key first appears in the source. Key equality follows the rules of Map; object keys are grouped by reference, not by value.

Aggregation

const characters = Query.from(notes.state.items).aggregate(0, (sum, note) => sum + note.body.length);
const hasPinned = Query.from(notes.state.items).any((note) => note.pinned && !note.archived);
const characters = Query.from(notes.state.items).aggregate(0, (sum, note) => sum + note.body.length);
const hasPinned = Query.from(notes.state.items).any((note) => note.pinned && !note.archived);

Use with reactivity

Query reads reactive arrays with ordinary array methods, so when it is used inside a getter, a createComputed or an effect, a dependency on the source is set up and the result is recomputed when the source changes.

In a list, the query is put in a getter and the rows are drawn with a list binding. In JSX the compiler turns the expression this.recent.map(...) (recent().map(...) in a function) into bindings.list, and without JSX bindings.list is called directly. Rows are matched to the item object, and the DOM nodes of the same object are reused; key takes no part in this matching, it identifies the item (see Conditional Rendering and Lists). The list in the example shows the five most recently edited notes:

import { Component, Query } from "@motifx/core";
import { NotesService } from "../services/notes";

export class RecentNotes extends Component<HTMLUListElement> {
  notes = this.getService(NotesService)!;

  get recent() {
    return Query.from(this.notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  }

  override view() {
    return <>{this.recent.map((note) => <li key={note.id}>{() => note.title || "Untitled"}</li>)}</>;
  }
}
import { Component, ComponentBase, Query, motifComponent } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export class RecentNotes extends Component<HTMLUListElement> {
  static override elementTag = "ul";
  notes = this.getService(NotesService)!;

  get recent() {
    return Query.from(this.notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  }

  override view() {
    return motifComponent({
      initializeComponent: (holder: ComponentBase) =>
        holder.bindings.list(() => this.recent, (note: Note) => motifComponent("li", {
          indexkey: () => note.id,
          initializeComponent: (li: ComponentBase) => li.bindings.text(() => note.title || "Untitled"),
        })),
    });
  }
}
import { FromService, Query } from "@motifx/core";
import { NotesService } from "../services/notes";

export function RecentNotes() {
  const notes = FromService(NotesService)!;
  const recent = () =>
    Query.from(notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);

  return <ul>{recent().map((note) => <li key={note.id}>{() => note.title || "Untitled"}</li>)}</ul>;
}
import { ComponentBase, FromService, Query, motifComponent } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export function RecentNotes() {
  const notes = FromService(NotesService)!;
  const recent = () =>
    Query.from(notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);

  return motifComponent("ul", (ul: ComponentBase) => {
    const rows = motifComponent({
      initializeComponent: (holder: ComponentBase) =>
        holder.bindings.list(() => recent(), (note: Note) => motifComponent("li", {
          indexkey: () => note.id,
          initializeComponent: (li: ComponentBase) => li.bindings.text(() => note.title || "Untitled"),
        })),
    });
    ul.controls.add(rows);
  });
}
import { Query, motifOptions } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export const RecentNotes = () => motifOptions({
  el: "ul",

  get recent(): Note[] {
    return Query.from(this.getService(NotesService)!.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  },

  view() {
    return <>{this.recent.map((note) => <li key={note.id}>{() => note.title || "Untitled"}</li>)}</>;
  },
});
import { ComponentBase, Query, motifComponent, motifOptions } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export const RecentNotes = () => motifOptions({
  el: "ul",

  get recent(): Note[] {
    return Query.from(this.getService(NotesService)!.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  },

  view() {
    return motifComponent({
      initializeComponent: (holder: ComponentBase) =>
        holder.bindings.list(() => this.recent, (note: Note) => motifComponent("li", {
          indexkey: () => note.id,
          initializeComponent: (li: ComponentBase) => li.bindings.text(() => note.title || "Untitled"),
        })),
    });
  },
});
import { Component, Query } from "@motifx/core";
import { NotesService } from "../services/notes";

export class RecentNotes extends Component {
  static elementTag = "ul";
  notes = this.getService(NotesService);

  get recent() {
    return Query.from(this.notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  }

  view() {
    return <>{this.recent.map((note) => <li key={note.id}>{() => note.title || "Untitled"}</li>)}</>;
  }
}
import { Component, Query, motifComponent } from "@motifx/core";
import { NotesService } from "../services/notes";

export class RecentNotes extends Component {
  static elementTag = "ul";
  notes = this.getService(NotesService);

  get recent() {
    return Query.from(this.notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  }

  view() {
    return motifComponent({
      initializeComponent: (holder) =>
        holder.bindings.list(() => this.recent, (note) => motifComponent("li", {
          indexkey: () => note.id,
          initializeComponent: (li) => li.bindings.text(() => note.title || "Untitled"),
        })),
    });
  }
}
import { FromService, Query } from "@motifx/core";
import { NotesService } from "../services/notes";

export function RecentNotes() {
  const notes = FromService(NotesService);
  const recent = () =>
    Query.from(notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);

  return <ul>{recent().map((note) => <li key={note.id}>{() => note.title || "Untitled"}</li>)}</ul>;
}
import { FromService, Query, motifComponent } from "@motifx/core";
import { NotesService } from "../services/notes";

export function RecentNotes() {
  const notes = FromService(NotesService);
  const recent = () =>
    Query.from(notes.state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);

  return motifComponent("ul", (ul) => {
    const rows = motifComponent({
      initializeComponent: (holder) =>
        holder.bindings.list(() => recent(), (note) => motifComponent("li", {
          indexkey: () => note.id,
          initializeComponent: (li) => li.bindings.text(() => note.title || "Untitled"),
        })),
    });
    ul.controls.add(rows);
  });
}
import { Query, motifOptions } from "@motifx/core";
import { NotesService } from "../services/notes";

export const RecentNotes = () => motifOptions({
  el: "ul",

  get recent() {
    return Query.from(this.getService(NotesService).state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  },

  view() {
    return <>{this.recent.map((note) => <li key={note.id}>{() => note.title || "Untitled"}</li>)}</>;
  },
});
import { Query, motifComponent, motifOptions } from "@motifx/core";
import { NotesService } from "../services/notes";

export const RecentNotes = () => motifOptions({
  el: "ul",

  get recent() {
    return Query.from(this.getService(NotesService).state.items)
      .where((note) => !note.archived)
      .orderByDescending((note) => note.updatedAt)
      .toArray()
      .slice(0, 5);
  },

  view() {
    return motifComponent({
      initializeComponent: (holder) =>
        holder.bindings.list(() => this.recent, (note) => motifComponent("li", {
          indexkey: () => note.id,
          initializeComponent: (li) => li.bindings.text(() => note.title || "Untitled"),
        })),
    });
  },
});

When a note is edited and its updatedAt changes, or when it is archived, the chain runs from the start and the list applies only the difference to the DOM. For very large lists, use the Virtualization component from the Virtual Lists section.

Things to know

Next step

For the List, Dictionary and LinkedList structures, go on to Collections.