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.

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:

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.