Core concepts
Async Results with doWork
`doWork`, which ties a promise to the component's lifetime: the returned promise, MJX108, `catch` and `finally`, how it differs from `using`.
doWork(promise) ties the given promise to the component's lifetime and returns a new promise. While the component is alive, this promise behaves like the one you gave it: the value goes to then and a rejection goes to catch. After the component is disposed the promise is not rejected; in place of the value or the rejection, a MotifError with the code MJX108 arrives. The difference from using is that it returns a promise: you can chain the result, add catch and finally, and hand it back to the caller.
The returned promise
| The given promise | If the component is alive | If the component has been disposed |
|---|---|---|
| resolves | then receives the value. |
then receives a MotifError (MJX108). |
| is rejected | catch receives the reason. |
then receives a MotifError (MJX108), with the rejection in that error's cause; catch does not run. |
MJX108is a return value: it is not reported toerrorHandlerand not written to the console. Write aninstanceof Errorcheck before using the value; if the check holds, the component is gone andthenreturns without doing anything.finallyruns in both cases. Code inside it that touches a field of the component writes to a cleared field if the component has been disposed; put that kind of work inthenandcatch.- An error thrown inside
thengoes to the nextcatchin the chain; that is how promises normally behave. - Any object with a
thenmethod can be given. - Whether the component is alive is checked with
isDisposedat the moment the result arrives; the flag becomestruewhen disposal finishes (see When the check happens).
The returned promise's type is Promise<T | MotifError>. After an instanceof Error (or instanceof MotifError) check the value narrows to T; using it without the check does not compile.
An example
The "Share" button on the note page asks the server for a share link. While the request is running the button is disabled; when the link arrives it becomes visible, and if the request fails a warning appears. If the user leaves the page before the response arrives, then receives MJX108 and returns without writing anything, and catch does not run:
import { Component, reactive } from "@motifx/core";
import type { Note } from "../services/notes";
type ShareLink = { url: string };
export class ShareButton extends Component<HTMLDivElement, { note: Note }> {
static override elementTag = "div";
state = reactive({ busy: false, url: "", failed: false });
share() {
this.state.busy = true;
this.state.failed = false;
this.doWork(fetch(`/api/notes/${this.props.note.id}/share`, { method: "POST" }).then((r) => r.json() as Promise<ShareLink>))
.then((link) => {
if (link instanceof Error) return;
this.state.url = link.url;
this.state.busy = false;
})
.catch(() => {
this.state.failed = true;
this.state.busy = false;
});
}
override onConfigured() {
this.class.add("share");
}
override view() {
return (
<>
<button type="button" disabled={() => this.state.busy} onclick={() => this.share()}>Share</button>
<a x-display={() => this.state.url !== ""} href={() => this.state.url}>Share link</a>
<small x-display={() => this.state.failed}>Couldn't share.</small>
</>
);
}
}
import { Component, ComponentBase, motifComponent, motifFragment, reactive } from "@motifx/core";
import type { Note } from "../services/notes";
type ShareLink = { url: string };
export class ShareButton extends Component<HTMLDivElement, { note: Note }> {
static override elementTag = "div";
state = reactive({ busy: false, url: "", failed: false });
share() {
this.state.busy = true;
this.state.failed = false;
this.doWork(fetch(`/api/notes/${this.props.note.id}/share`, { method: "POST" }).then((r) => r.json() as Promise<ShareLink>))
.then((link) => {
if (link instanceof Error) return;
this.state.url = link.url;
this.state.busy = false;
})
.catch(() => {
this.state.failed = true;
this.state.busy = false;
});
}
override onConfigured() {
this.class.add("share");
}
override view() {
const button = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.bindings.add("disabled", () => this.state.busy);
button.motif.on("click", () => this.share());
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Share")));
});
const link = motifComponent("a", {
preconfig: (a: ComponentBase) => a.bindings.display(() => this.state.url !== ""),
initializeComponent: (a: ComponentBase) => {
a.bindings.add("href", () => this.state.url);
a.setText("Share link");
},
});
const failed = motifComponent("small", {
preconfig: (small: ComponentBase) => small.bindings.display(() => this.state.failed),
initializeComponent: (small: ComponentBase) => small.setText("Couldn't share."),
});
return motifFragment({ childs: [button, link, failed] });
}
}
import { reactive } from "@motifx/core";
import type { Note } from "../services/notes";
type ShareLink = { url: string };
export function ShareButton(props: { note: Note }) {
const state = reactive({ busy: false, url: "", failed: false });
const share = () => {
state.busy = true;
state.failed = false;
box.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json() as Promise<ShareLink>))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
};
const box = (
<div class="share">
<button type="button" disabled={() => state.busy} onclick={() => share()}>Share</button>
<a x-display={() => state.url !== ""} href={() => state.url}>Share link</a>
<small x-display={() => state.failed}>Couldn't share.</small>
</div>
);
return box;
}
import { ComponentBase, motifComponent, reactive } from "@motifx/core";
import type { Note } from "../services/notes";
type ShareLink = { url: string };
export function ShareButton(props: { note: Note }) {
const state = reactive({ busy: false, url: "", failed: false });
const share = () => {
state.busy = true;
state.failed = false;
box.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json() as Promise<ShareLink>))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
};
const box: ComponentBase = motifComponent("div", (div: ComponentBase) => {
div.class.add("share");
const button = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.bindings.add("disabled", () => state.busy);
button.motif.on("click", () => share());
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Share")));
});
const link = motifComponent("a", {
preconfig: (a: ComponentBase) => a.bindings.display(() => state.url !== ""),
initializeComponent: (a: ComponentBase) => {
a.bindings.add("href", () => state.url);
a.setText("Share link");
},
});
const failed = motifComponent("small", {
preconfig: (small: ComponentBase) => small.bindings.display(() => state.failed),
initializeComponent: (small: ComponentBase) => small.setText("Couldn't share."),
});
div.controls.add(button, link, failed);
});
return box;
}
import { motifOptions, reactive } from "@motifx/core";
import type { Note } from "../services/notes";
type ShareLink = { url: string };
export const ShareButton = (props: { note: Note }) => motifOptions({
el: "div",
data: reactive({ busy: false, url: "", failed: false }),
share() {
const state = this.data;
state.busy = true;
state.failed = false;
this.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json() as Promise<ShareLink>))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
},
onConfigured() {
this.class.add("share");
},
view() {
const state = this.data;
return (
<>
<button type="button" disabled={() => state.busy} onclick={() => this.share()}>Share</button>
<a x-display={() => state.url !== ""} href={() => state.url}>Share link</a>
<small x-display={() => state.failed}>Couldn't share.</small>
</>
);
},
});
import { ComponentBase, motifComponent, motifFragment, motifOptions, reactive } from "@motifx/core";
import type { Note } from "../services/notes";
type ShareLink = { url: string };
export const ShareButton = (props: { note: Note }) => motifOptions({
el: "div",
data: reactive({ busy: false, url: "", failed: false }),
share() {
const state = this.data;
state.busy = true;
state.failed = false;
this.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json() as Promise<ShareLink>))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
},
onConfigured() {
this.class.add("share");
},
view() {
const state = this.data;
const button = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.bindings.add("disabled", () => state.busy);
button.motif.on("click", () => this.share());
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Share")));
});
const link = motifComponent("a", {
preconfig: (a: ComponentBase) => a.bindings.display(() => state.url !== ""),
initializeComponent: (a: ComponentBase) => {
a.bindings.add("href", () => state.url);
a.setText("Share link");
},
});
const failed = motifComponent("small", {
preconfig: (small: ComponentBase) => small.bindings.display(() => state.failed),
initializeComponent: (small: ComponentBase) => small.setText("Couldn't share."),
});
return motifFragment({ childs: [button, link, failed] });
},
});
import { Component, reactive } from "@motifx/core";
export class ShareButton extends Component {
static elementTag = "div";
state = reactive({ busy: false, url: "", failed: false });
share() {
this.state.busy = true;
this.state.failed = false;
this.doWork(fetch(`/api/notes/${this.props.note.id}/share`, { method: "POST" }).then((r) => r.json()))
.then((link) => {
if (link instanceof Error) return;
this.state.url = link.url;
this.state.busy = false;
})
.catch(() => {
this.state.failed = true;
this.state.busy = false;
});
}
onConfigured() {
this.class.add("share");
}
view() {
return (
<>
<button type="button" disabled={() => this.state.busy} onclick={() => this.share()}>Share</button>
<a x-display={() => this.state.url !== ""} href={() => this.state.url}>Share link</a>
<small x-display={() => this.state.failed}>Couldn't share.</small>
</>
);
}
}
import { Component, motifComponent, motifFragment, reactive } from "@motifx/core";
export class ShareButton extends Component {
static elementTag = "div";
state = reactive({ busy: false, url: "", failed: false });
share() {
this.state.busy = true;
this.state.failed = false;
this.doWork(fetch(`/api/notes/${this.props.note.id}/share`, { method: "POST" }).then((r) => r.json()))
.then((link) => {
if (link instanceof Error) return;
this.state.url = link.url;
this.state.busy = false;
})
.catch(() => {
this.state.failed = true;
this.state.busy = false;
});
}
onConfigured() {
this.class.add("share");
}
view() {
const button = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.bindings.add("disabled", () => this.state.busy);
button.motif.on("click", () => this.share());
button.controls.add(motifComponent("text", (node) => node.setText("Share")));
});
const link = motifComponent("a", {
preconfig: (a) => a.bindings.display(() => this.state.url !== ""),
initializeComponent: (a) => {
a.bindings.add("href", () => this.state.url);
a.setText("Share link");
},
});
const failed = motifComponent("small", {
preconfig: (small) => small.bindings.display(() => this.state.failed),
initializeComponent: (small) => small.setText("Couldn't share."),
});
return motifFragment({ childs: [button, link, failed] });
}
}
import { reactive } from "@motifx/core";
export function ShareButton(props) {
const state = reactive({ busy: false, url: "", failed: false });
const share = () => {
state.busy = true;
state.failed = false;
box.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json()))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
};
const box = (
<div class="share">
<button type="button" disabled={() => state.busy} onclick={() => share()}>Share</button>
<a x-display={() => state.url !== ""} href={() => state.url}>Share link</a>
<small x-display={() => state.failed}>Couldn't share.</small>
</div>
);
return box;
}
import { motifComponent, reactive } from "@motifx/core";
export function ShareButton(props) {
const state = reactive({ busy: false, url: "", failed: false });
const share = () => {
state.busy = true;
state.failed = false;
box.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json()))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
};
const box = motifComponent("div", (div) => {
div.class.add("share");
const button = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.bindings.add("disabled", () => state.busy);
button.motif.on("click", () => share());
button.controls.add(motifComponent("text", (node) => node.setText("Share")));
});
const link = motifComponent("a", {
preconfig: (a) => a.bindings.display(() => state.url !== ""),
initializeComponent: (a) => {
a.bindings.add("href", () => state.url);
a.setText("Share link");
},
});
const failed = motifComponent("small", {
preconfig: (small) => small.bindings.display(() => state.failed),
initializeComponent: (small) => small.setText("Couldn't share."),
});
div.controls.add(button, link, failed);
});
return box;
}
import { motifOptions, reactive } from "@motifx/core";
export const ShareButton = (props) => motifOptions({
el: "div",
data: reactive({ busy: false, url: "", failed: false }),
share() {
const state = this.data;
state.busy = true;
state.failed = false;
this.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json()))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
},
onConfigured() {
this.class.add("share");
},
view() {
const state = this.data;
return (
<>
<button type="button" disabled={() => state.busy} onclick={() => this.share()}>Share</button>
<a x-display={() => state.url !== ""} href={() => state.url}>Share link</a>
<small x-display={() => state.failed}>Couldn't share.</small>
</>
);
},
});
import { motifComponent, motifFragment, motifOptions, reactive } from "@motifx/core";
export const ShareButton = (props) => motifOptions({
el: "div",
data: reactive({ busy: false, url: "", failed: false }),
share() {
const state = this.data;
state.busy = true;
state.failed = false;
this.doWork(fetch(`/api/notes/${props.note.id}/share`, { method: "POST" }).then((r) => r.json()))
.then((link) => {
if (link instanceof Error) return;
state.url = link.url;
state.busy = false;
})
.catch(() => {
state.failed = true;
state.busy = false;
});
},
onConfigured() {
this.class.add("share");
},
view() {
const state = this.data;
const button = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.bindings.add("disabled", () => state.busy);
button.motif.on("click", () => this.share());
button.controls.add(motifComponent("text", (node) => node.setText("Share")));
});
const link = motifComponent("a", {
preconfig: (a) => a.bindings.display(() => state.url !== ""),
initializeComponent: (a) => {
a.bindings.add("href", () => state.url);
a.setText("Share link");
},
});
const failed = motifComponent("small", {
preconfig: (small) => small.bindings.display(() => state.failed),
initializeComponent: (small) => small.setText("Couldn't share."),
});
return motifFragment({ childs: [button, link, failed] });
},
});
share() is a method of the component, and so is doWork, called as this.doWork(…).
A function component has no this; doWork is a method of the returned root. share uses the root under the name box; by the time the button is clicked, the root has long been built.
The note page places the button with the open note: <ShareButton note={note} />.
The note page places the button with the open note: new ShareButton({ note }).
The note page places the button with the open note: motifComponent(ShareButton, { note }).
How it differs from using
doWork hands the result back as a promise; using finishes the result in the handlers it is given and returns nothing. All the details below follow from that difference:
doWork |
using |
|
|---|---|---|
| Returns | A promise; you add then, catch, finally, and it can be returned to the caller. |
Nothing; the work ends in the handlers given in the call. |
| Incoming value | then receives either the value or MJX108; an instanceof Error check is needed before using the value. |
onfulfilled receives the value itself; no check is needed. |
| After disposal | then receives MJX108, catch does not run, finally runs. |
No handler runs. |
| Rejection while the component is alive | Goes to catch; without a catch it becomes an unhandled promise rejection. |
Goes to onrejected; without it, it is swallowed quietly. |
| An error thrown by a handler | Goes to the next catch in the chain; without one it becomes an unhandled promise rejection. |
Reported to errorHandler with MJX129. |
Which one when
Pick doWork when:
- Another piece of work follows the first; for example saving the note and then reloading the list.
thenstarts the second request withdoWorkagain and returns it; ifMJX108arrives at the first step,thenreturns early through theinstanceof Errorcheck and the second request never goes out. A singlecatchat the end of the chain catches the rejection of both steps. - You wait for several requests to finish together:
doWork(Promise.all([…]))gives the results as an array; if the component has been disposed, a singleMJX108arrives. - 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.finallyruns after the component is disposed as well.
If the result is only written to this component's state and nobody waits for the work to end, using is shorter: no instanceof Error check or catch is needed, and an error thrown by a handler goes to errorHandler on its own.
doWork does not stop the request either; to cancel the request when the component is disposed, see The request keeps running.
Next step
For the details of writing a component's view, go on to JSX and Templates.