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
- Evaluation is immediate. Every step of the chain produces its own intermediate array; there is no lazy evaluation. Up to a few thousand items, the cost of this is negligible.
- The source does not change.
orderByandorderByDescendingsort a copy of the source;toArray()returns a new array on every call. - The result is a snapshot. Every step runs at the moment it is called; items added to the source later do not show up in step results computed earlier. (
Query.from(array)with no steps applied does not copy the array, so it reads the array's current state; a source that is not an array is copied at the moment offrom.) For an up-to-date result, set up the chain inside a getter and run it again on every read. - The chain returns
Query. The result of steps such aswhere,selectandgroupByis aQuery, not an array (groupBy→Query<Group<TKey, T>>); where an array is needed, addtoArray()at the end.
Next step
For the List, Dictionary and LinkedList structures, go on to Collections.