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.

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:

doWork is needed when:

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.