Core concepts
Async Results with using
`using`, which drops a result that arrives after the component is disposed: the call form, handlers, MJX129 and when the check happens.
When a component asks the server for data, there is no telling when the response will arrive, and by then the component may have been disposed. A disposed component's bindings stop, and the fields of a component whose dispose() was called are cleared as well (see What is released on its own). Code that writes a late response to that component shows nothing, and if the field has been cleared it throws a TypeError. using(promise, onfulfilled?, onrejected?) passes the promise's result to the handlers only if the component is still alive; if the component has been disposed, it drops the result.
Call form
| Parameter | What it takes |
|---|---|
promise |
A Promise, or any object with a then method. |
onfulfilled(value) |
Called when the promise resolves, if the component is alive. |
onrejected(reason) |
Called when the promise is rejected, if the component is alive. |
- If the component has been disposed, neither handler is called; both the result and the rejection are dropped.
- If
onrejectedis not given, a rejection is swallowed quietly: it neither goes toerrorHandlernor shows up as an unhandled promise rejection. If you want to see the error, writeonrejected. usingreturns nothing. If you need to wait for the result, chain it or hand it back to the caller, use doWork.- The value a handler returns is not used. If a handler throws, or the promise of an
asynchandler is rejected, the error is reported toerrorHandlerwith the codeMJX129, and the original error is incause(see Error Handling).
The value's type comes from the promise: for a promise that gives Promise<SharedNote[]>, the parameter of onfulfilled is SharedNote[]. With a promise that returns any, such as r.json(), the type is lost; supply it with as Promise<SharedNote[]> as in the example below, or by writing a type on the parameter. Handlers can also be written with a body; they do not have to return a value.
An example
The "shared with me" section in the sidebar asks the server for the list while it is being built. When the response arrives the list fills in, and if the request fails a warning appears. If the user moves to another page before the response arrives and the section is disposed, neither handler runs:
import { Component, reactive } from "@motifx/core";
type SharedNote = { id: number; title: string; owner: string };
export class SharedNotes extends Component {
static override elementTag = "section";
state = reactive({ items: [] as SharedNote[], loading: true, failed: false });
override onConfigured() {
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json() as Promise<SharedNote[]>),
(items) => {
this.state.items = items;
this.state.loading = false;
},
() => {
this.state.failed = true;
this.state.loading = false;
},
);
}
override view() {
return (
<>
<h2>Shared with me</h2>
<p x-display={() => this.state.loading}>Loading…</p>
<p x-display={() => this.state.failed}>Couldn't load shared notes.</p>
<ul>
{this.state.items.map((note) => (
<li key={note.id}>
<span>{note.title}</span>
<small>{note.owner}</small>
</li>
))}
</ul>
</>
);
}
}
import { Component, ComponentBase, motifComponent, motifFragment, reactive } from "@motifx/core";
type SharedNote = { id: number; title: string; owner: string };
export class SharedNotes extends Component {
static override elementTag = "section";
state = reactive({ items: [] as SharedNote[], loading: true, failed: false });
override onConfigured() {
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json() as Promise<SharedNote[]>),
(items) => {
this.state.items = items;
this.state.loading = false;
},
() => {
this.state.failed = true;
this.state.loading = false;
},
);
}
override view() {
const title = motifComponent("h2", (h2: ComponentBase) => h2.setText("Shared with me"));
const loading = motifComponent("p", {
preconfig: (p: ComponentBase) => p.bindings.display(() => this.state.loading),
initializeComponent: (p: ComponentBase) => p.setText("Loading…"),
});
const failed = motifComponent("p", {
preconfig: (p: ComponentBase) => p.bindings.display(() => this.state.failed),
initializeComponent: (p: ComponentBase) => p.setText("Couldn't load shared notes."),
});
const list = motifComponent("ul", (ul: ComponentBase) => {
ul.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => this.state.items, (note: SharedNote) => this.row(note)),
}));
});
return motifFragment({ childs: [title, loading, failed, list] });
}
private row(note: SharedNote) {
return motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li: ComponentBase) => {
li.controls.add(
motifComponent("span", (span: ComponentBase) => span.setText(note.title)),
motifComponent("small", (small: ComponentBase) => small.setText(note.owner)),
);
},
});
}
}
import { reactive } from "@motifx/core";
type SharedNote = { id: number; title: string; owner: string };
export function SharedNotes() {
const state = reactive({ items: [] as SharedNote[], loading: true, failed: false });
const section = (
<section class="shared">
<h2>Shared with me</h2>
<p x-display={() => state.loading}>Loading…</p>
<p x-display={() => state.failed}>Couldn't load shared notes.</p>
<ul>
{state.items.map((note) => (
<li key={note.id}>
<span>{note.title}</span>
<small>{note.owner}</small>
</li>
))}
</ul>
</section>
);
section.using(
fetch("/api/notes/shared").then((r) => r.json() as Promise<SharedNote[]>),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
return section;
}
import { ComponentBase, motifComponent, reactive } from "@motifx/core";
type SharedNote = { id: number; title: string; owner: string };
export function SharedNotes() {
const state = reactive({ items: [] as SharedNote[], loading: true, failed: false });
const row = (note: SharedNote) =>
motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li: ComponentBase) => {
li.controls.add(
motifComponent("span", (span: ComponentBase) => span.setText(note.title)),
motifComponent("small", (small: ComponentBase) => small.setText(note.owner)),
);
},
});
const section: ComponentBase = motifComponent("section", (root: ComponentBase) => {
root.class.add("shared");
const title = motifComponent("h2", (h2: ComponentBase) => h2.setText("Shared with me"));
const loading = motifComponent("p", {
preconfig: (p: ComponentBase) => p.bindings.display(() => state.loading),
initializeComponent: (p: ComponentBase) => p.setText("Loading…"),
});
const failed = motifComponent("p", {
preconfig: (p: ComponentBase) => p.bindings.display(() => state.failed),
initializeComponent: (p: ComponentBase) => p.setText("Couldn't load shared notes."),
});
const list = motifComponent("ul", (ul: ComponentBase) => {
ul.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => state.items, (note: SharedNote) => row(note)),
}));
});
root.controls.add(title, loading, failed, list);
});
section.using(
fetch("/api/notes/shared").then((r) => r.json() as Promise<SharedNote[]>),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
return section;
}
import { motifOptions, reactive } from "@motifx/core";
type SharedNote = { id: number; title: string; owner: string };
export const SharedNotes = () => motifOptions({
el: "section",
data: reactive({ items: [] as SharedNote[], loading: true, failed: false }),
onConfigured() {
const state = this.data;
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json() as Promise<SharedNote[]>),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
},
view() {
const state = this.data;
return (
<>
<h2>Shared with me</h2>
<p x-display={() => state.loading}>Loading…</p>
<p x-display={() => state.failed}>Couldn't load shared notes.</p>
<ul>
{state.items.map((note) => (
<li key={note.id}>
<span>{note.title}</span>
<small>{note.owner}</small>
</li>
))}
</ul>
</>
);
},
});
import { ComponentBase, motifComponent, motifFragment, motifOptions, reactive } from "@motifx/core";
type SharedNote = { id: number; title: string; owner: string };
export const SharedNotes = () => motifOptions({
el: "section",
data: reactive({ items: [] as SharedNote[], loading: true, failed: false }),
onConfigured() {
const state = this.data;
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json() as Promise<SharedNote[]>),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
},
view() {
const state = this.data;
const row = (note: SharedNote) =>
motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li: ComponentBase) => {
li.controls.add(
motifComponent("span", (span: ComponentBase) => span.setText(note.title)),
motifComponent("small", (small: ComponentBase) => small.setText(note.owner)),
);
},
});
const title = motifComponent("h2", (h2: ComponentBase) => h2.setText("Shared with me"));
const loading = motifComponent("p", {
preconfig: (p: ComponentBase) => p.bindings.display(() => state.loading),
initializeComponent: (p: ComponentBase) => p.setText("Loading…"),
});
const failed = motifComponent("p", {
preconfig: (p: ComponentBase) => p.bindings.display(() => state.failed),
initializeComponent: (p: ComponentBase) => p.setText("Couldn't load shared notes."),
});
const list = motifComponent("ul", (ul: ComponentBase) => {
ul.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => state.items, (note: SharedNote) => row(note)),
}));
});
return motifFragment({ childs: [title, loading, failed, list] });
},
});
import { Component, reactive } from "@motifx/core";
export class SharedNotes extends Component {
static elementTag = "section";
state = reactive({ items: [], loading: true, failed: false });
onConfigured() {
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json()),
(items) => {
this.state.items = items;
this.state.loading = false;
},
() => {
this.state.failed = true;
this.state.loading = false;
},
);
}
view() {
return (
<>
<h2>Shared with me</h2>
<p x-display={() => this.state.loading}>Loading…</p>
<p x-display={() => this.state.failed}>Couldn't load shared notes.</p>
<ul>
{this.state.items.map((note) => (
<li key={note.id}>
<span>{note.title}</span>
<small>{note.owner}</small>
</li>
))}
</ul>
</>
);
}
}
import { Component, motifComponent, motifFragment, reactive } from "@motifx/core";
export class SharedNotes extends Component {
static elementTag = "section";
state = reactive({ items: [], loading: true, failed: false });
onConfigured() {
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json()),
(items) => {
this.state.items = items;
this.state.loading = false;
},
() => {
this.state.failed = true;
this.state.loading = false;
},
);
}
view() {
const title = motifComponent("h2", (h2) => h2.setText("Shared with me"));
const loading = motifComponent("p", {
preconfig: (p) => p.bindings.display(() => this.state.loading),
initializeComponent: (p) => p.setText("Loading…"),
});
const failed = motifComponent("p", {
preconfig: (p) => p.bindings.display(() => this.state.failed),
initializeComponent: (p) => p.setText("Couldn't load shared notes."),
});
const list = motifComponent("ul", (ul) => {
ul.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => this.state.items, (note) => this.row(note)),
}));
});
return motifFragment({ childs: [title, loading, failed, list] });
}
row(note) {
return motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li) => {
li.controls.add(
motifComponent("span", (span) => span.setText(note.title)),
motifComponent("small", (small) => small.setText(note.owner)),
);
},
});
}
}
import { reactive } from "@motifx/core";
export function SharedNotes() {
const state = reactive({ items: [], loading: true, failed: false });
const section = (
<section class="shared">
<h2>Shared with me</h2>
<p x-display={() => state.loading}>Loading…</p>
<p x-display={() => state.failed}>Couldn't load shared notes.</p>
<ul>
{state.items.map((note) => (
<li key={note.id}>
<span>{note.title}</span>
<small>{note.owner}</small>
</li>
))}
</ul>
</section>
);
section.using(
fetch("/api/notes/shared").then((r) => r.json()),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
return section;
}
import { motifComponent, reactive } from "@motifx/core";
export function SharedNotes() {
const state = reactive({ items: [], loading: true, failed: false });
const row = (note) =>
motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li) => {
li.controls.add(
motifComponent("span", (span) => span.setText(note.title)),
motifComponent("small", (small) => small.setText(note.owner)),
);
},
});
const section = motifComponent("section", (root) => {
root.class.add("shared");
const title = motifComponent("h2", (h2) => h2.setText("Shared with me"));
const loading = motifComponent("p", {
preconfig: (p) => p.bindings.display(() => state.loading),
initializeComponent: (p) => p.setText("Loading…"),
});
const failed = motifComponent("p", {
preconfig: (p) => p.bindings.display(() => state.failed),
initializeComponent: (p) => p.setText("Couldn't load shared notes."),
});
const list = motifComponent("ul", (ul) => {
ul.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => state.items, (note) => row(note)),
}));
});
root.controls.add(title, loading, failed, list);
});
section.using(
fetch("/api/notes/shared").then((r) => r.json()),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
return section;
}
import { motifOptions, reactive } from "@motifx/core";
export const SharedNotes = () => motifOptions({
el: "section",
data: reactive({ items: [], loading: true, failed: false }),
onConfigured() {
const state = this.data;
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json()),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
},
view() {
const state = this.data;
return (
<>
<h2>Shared with me</h2>
<p x-display={() => state.loading}>Loading…</p>
<p x-display={() => state.failed}>Couldn't load shared notes.</p>
<ul>
{state.items.map((note) => (
<li key={note.id}>
<span>{note.title}</span>
<small>{note.owner}</small>
</li>
))}
</ul>
</>
);
},
});
import { motifComponent, motifFragment, motifOptions, reactive } from "@motifx/core";
export const SharedNotes = () => motifOptions({
el: "section",
data: reactive({ items: [], loading: true, failed: false }),
onConfigured() {
const state = this.data;
this.class.add("shared");
this.using(
fetch("/api/notes/shared").then((r) => r.json()),
(items) => {
state.items = items;
state.loading = false;
},
() => {
state.failed = true;
state.loading = false;
},
);
},
view() {
const state = this.data;
const row = (note) =>
motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li) => {
li.controls.add(
motifComponent("span", (span) => span.setText(note.title)),
motifComponent("small", (small) => small.setText(note.owner)),
);
},
});
const title = motifComponent("h2", (h2) => h2.setText("Shared with me"));
const loading = motifComponent("p", {
preconfig: (p) => p.bindings.display(() => state.loading),
initializeComponent: (p) => p.setText("Loading…"),
});
const failed = motifComponent("p", {
preconfig: (p) => p.bindings.display(() => state.failed),
initializeComponent: (p) => p.setText("Couldn't load shared notes."),
});
const list = motifComponent("ul", (ul) => {
ul.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => state.items, (note) => row(note)),
}));
});
return motifFragment({ childs: [title, loading, failed, list] });
},
});
The request starts in onConfigured: the component's settings are ready, and the element does not need to be in the document. using is a method of the component and is called as this.using(…).
A function component has no this; using is a method of the returned root. The body builds the root, ties the request to it with section.using(…) and returns the root. The body runs once, while the component is being built, so the request goes out once as well.
The sidebar places the section with <SharedNotes />.
The sidebar places the section with new SharedNotes().
The sidebar places the section with motifComponent(SharedNotes).
How it differs from doWork
using finishes the result in the handlers it is given; doWork hands the result back as a promise. All the details below follow from that difference:
using |
doWork |
|
|---|---|---|
| Returns | Nothing; the work ends in the handlers given in the call. | A promise; you add then, catch, finally, and it can be returned to the caller. |
| Incoming value | onfulfilled receives the value itself; no check is needed. |
then receives either the value or MJX108; an instanceof Error check is needed before using the value. |
| After disposal | No handler runs. | then receives MJX108, catch does not run, finally runs. |
| Rejection while the component is alive | Goes to onrejected; without it, it is swallowed quietly. |
Goes to catch; without a catch it becomes an unhandled promise rejection. |
| An error thrown by a handler | Reported to errorHandler with MJX129. |
Goes to the next catch in the chain; without one it becomes an unhandled promise rejection. |
Which one when
using is enough when:
- The result is only written to this component's state: loading a list on startup, refreshing in the background, fetching the content of a preview.
- Nobody outside the component needs to know when the work is done.
- One handler for success and one for failure are enough. Deliberately ignoring an error is also done by leaving
onrejectedout.
doWork is needed when:
- Another piece of work follows the first; for example saving the note and then reloading the list.
- You wait for several requests to finish together:
doWork(Promise.all([…])). - The method that starts the work is going to return the promise, so a parent component, a keyboard shortcut or a test can wait for the work to finish.
- You need
finally, or an error carried along the chain.
When the check happens
using checks whether the component is alive at the moment the result arrives, through the isDisposed flag. This flag becomes true when disposal finishes, not at the moment dispose() is called. A result that arrives while disposal is in progress, such as the result of a promise that resolved just before dispose(), is still handled; nothing changes on screen, because the component's bindings have already stopped by then.
A component that is hidden (motif.hide(), x-display), detached from the tree (controls.detach) or moved somewhere else does not count as disposed, and its result is handled.
The request keeps running
using does not stop the promise: even when the component is disposed, the request to the server keeps going and only its result is dropped. If the request has to stop too, set up an AbortController, pass its signal to fetch, and tie the abort() call to the component's lifetime with motif.setDisposable(() => controller.abort()) (see Resources tied by hand). The setDisposable handler runs after isDisposed has become true. So using drops the cancelled request's AbortError rejection as well, and onrejected is not called. In the same situation doWork gives MJX108, with the AbortError in its cause.
Next step
For work that waits for the result or chains it, go on to Async Results with doWork.