Dependency Injection

Lifetimes and Scopes

singleton, scoped and transient; the navigation scope and scoped handles.

Lifetimes

Lifetime Behaviour
singleton A single instance for the whole application.
scoped One instance per navigation. Everything on screen in the same navigation (the layout, the page, child components, FromService, singleton dependencies) sees the same instance; details below. Without a router, it is a single instance in the root provider.
transient A new instance on every resolution (the default).

The lifetime is given on the registration (addSingleton / addScoped / addTransient, or @Injectable({ lifetime }); see Registering Services). If a route component itself is registered in DI, the registration's lifetime decides the page's lifetime (see How a route component is built).

scoped: a single instance per navigation

The router opens a scope on every navigation. Everyone who asks for a scoped service (getService, inject, deps, FromService) gets not the service itself but a handle that leads to it. The handle is used like the service (fields, methods, #private fields, instanceof) and goes to the instance of the right navigation on every access. In the example, the layout and the page use the same scoped selection:

import { Injectable, reactive } from "@motifx/core";

@Injectable({ lifetime: "scoped" })
export class Selection {
  ids = reactive<number[]>([]);

  toggle(id: number) {
    const i = this.ids.indexOf(id);
    if (i < 0) this.ids.push(id);
    else this.ids.splice(i, 1);
  }
}
import { Injectable, reactive } from "@motifx/core";

@Injectable({ lifetime: "scoped" })
export class Selection {
  ids = reactive([]);

  toggle(id) {
    const i = this.ids.indexOf(id);
    if (i < 0) this.ids.push(id);
    else this.ids.splice(i, 1);
  }
}

The counter in the layout and the button on the note page ask for the same service:

import { Component } from "@motifx/core";
import { Selection } from "../services/selection";
import type { Note } from "../services/notes";

export class SelectedCount extends Component {
  selection = this.getService(Selection)!;

  override view() {
    return <span class="badge">{() => `${this.selection.ids.length} selected`}</span>;
  }
}

export class SelectButton extends Component<any, { note: Note }> {
  selection = this.getService(Selection)!;

  override view() {
    const { note } = this.props;
    return (
      <button type="button" class="button--sm ghost" onclick={() => this.selection.toggle(note.id)}>
        Select
      </button>
    );
  }
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import { Selection } from "../services/selection";
import type { Note } from "../services/notes";

export class SelectedCount extends Component {
  selection = this.getService(Selection)!;

  override view() {
    return motifComponent("span", (span: ComponentBase) => {
      span.class.add("badge");
      span.bindings.text(() => `${this.selection.ids.length} selected`);
    });
  }
}

export class SelectButton extends Component<any, { note: Note }> {
  selection = this.getService(Selection)!;

  override view() {
    const { note } = this.props;
    return motifComponent("button", (button: ComponentBase) => {
      button.attr.add({ type: "button" });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => this.selection.toggle(note.id));
      button.setText("Select");
    });
  }
}
import { FromService } from "@motifx/core";
import { Selection } from "../services/selection";
import type { Note } from "../services/notes";

export function SelectedCount() {
  const selection = FromService(Selection)!;

  return <span class="badge">{() => `${selection.ids.length} selected`}</span>;
}

export function SelectButton(props: { note: Note }) {
  const selection = FromService(Selection)!;
  const { note } = props;

  return (
    <button type="button" class="button--sm ghost" onclick={() => selection.toggle(note.id)}>
      Select
    </button>
  );
}
import { ComponentBase, FromService, motifComponent } from "@motifx/core";
import { Selection } from "../services/selection";
import type { Note } from "../services/notes";

export function SelectedCount() {
  const selection = FromService(Selection)!;

  return motifComponent("span", (span: ComponentBase) => {
    span.class.add("badge");
    span.bindings.text(() => `${selection.ids.length} selected`);
  });
}

export function SelectButton(props: { note: Note }) {
  const selection = FromService(Selection)!;
  const { note } = props;

  return motifComponent("button", (button: ComponentBase) => {
    button.attr.add({ type: "button" });
    button.class.add("button--sm ghost");
    button.motif.on("click", () => selection.toggle(note.id));
    button.setText("Select");
  });
}
import { motifOptions } from "@motifx/core";
import { Selection } from "../services/selection";
import type { Note } from "../services/notes";

export const SelectedCount = () => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection)!;
    return <span class="badge">{() => `${selection.ids.length} selected`}</span>;
  },
});

export const SelectButton = (props: { note: Note }) => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection)!;
    const { note } = props;
    return (
      <button type="button" class="button--sm ghost" onclick={() => selection.toggle(note.id)}>
        Select
      </button>
    );
  },
});
import { ComponentBase, motifComponent, motifOptions } from "@motifx/core";
import { Selection } from "../services/selection";
import type { Note } from "../services/notes";

export const SelectedCount = () => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection)!;
    return motifComponent("span", (span: ComponentBase) => {
      span.class.add("badge");
      span.bindings.text(() => `${selection.ids.length} selected`);
    });
  },
});

export const SelectButton = (props: { note: Note }) => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection)!;
    const { note } = props;
    return motifComponent("button", (button: ComponentBase) => {
      button.attr.add({ type: "button" });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => selection.toggle(note.id));
      button.setText("Select");
    });
  },
});
import { Component } from "@motifx/core";
import { Selection } from "../services/selection";

export class SelectedCount extends Component {
  selection = this.getService(Selection);

  view() {
    return <span class="badge">{() => `${this.selection.ids.length} selected`}</span>;
  }
}

export class SelectButton extends Component {
  selection = this.getService(Selection);

  view() {
    const { note } = this.props;
    return (
      <button type="button" class="button--sm ghost" onclick={() => this.selection.toggle(note.id)}>
        Select
      </button>
    );
  }
}
import { Component, motifComponent } from "@motifx/core";
import { Selection } from "../services/selection";

export class SelectedCount extends Component {
  selection = this.getService(Selection);

  view() {
    return motifComponent("span", (span) => {
      span.class.add("badge");
      span.bindings.text(() => `${this.selection.ids.length} selected`);
    });
  }
}

export class SelectButton extends Component {
  selection = this.getService(Selection);

  view() {
    const { note } = this.props;
    return motifComponent("button", (button) => {
      button.attr.add({ type: "button" });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => this.selection.toggle(note.id));
      button.setText("Select");
    });
  }
}
import { FromService } from "@motifx/core";
import { Selection } from "../services/selection";

export function SelectedCount() {
  const selection = FromService(Selection);

  return <span class="badge">{() => `${selection.ids.length} selected`}</span>;
}

export function SelectButton(props) {
  const selection = FromService(Selection);
  const { note } = props;

  return (
    <button type="button" class="button--sm ghost" onclick={() => selection.toggle(note.id)}>
      Select
    </button>
  );
}
import { FromService, motifComponent } from "@motifx/core";
import { Selection } from "../services/selection";

export function SelectedCount() {
  const selection = FromService(Selection);

  return motifComponent("span", (span) => {
    span.class.add("badge");
    span.bindings.text(() => `${selection.ids.length} selected`);
  });
}

export function SelectButton(props) {
  const selection = FromService(Selection);
  const { note } = props;

  return motifComponent("button", (button) => {
    button.attr.add({ type: "button" });
    button.class.add("button--sm ghost");
    button.motif.on("click", () => selection.toggle(note.id));
    button.setText("Select");
  });
}
import { motifOptions } from "@motifx/core";
import { Selection } from "../services/selection";

export const SelectedCount = () => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection);
    return <span class="badge">{() => `${selection.ids.length} selected`}</span>;
  },
});

export const SelectButton = (props) => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection);
    const { note } = props;
    return (
      <button type="button" class="button--sm ghost" onclick={() => selection.toggle(note.id)}>
        Select
      </button>
    );
  },
});
import { motifComponent, motifOptions } from "@motifx/core";
import { Selection } from "../services/selection";

export const SelectedCount = () => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection);
    return motifComponent("span", (span) => {
      span.class.add("badge");
      span.bindings.text(() => `${selection.ids.length} selected`);
    });
  },
});

export const SelectButton = (props) => motifOptions({
  el: null,

  view() {
    const selection = this.getService(Selection);
    const { note } = props;
    return motifComponent("button", (button) => {
      button.attr.add({ type: "button" });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => selection.toggle(note.id));
      button.setText("Select");
    });
  },
});

The layout places the counter with <SelectedCount />, and the note page places the button with <SelectButton note={note} />.

The layout places the counter with new SelectedCount(), and the note page places the button with new SelectButton({ note }).

The layout places the counter with motifComponent(SelectedCount), and the note page places the button with motifComponent(SelectButton, { note }).

Since the fields of a disposed component are cleared, checking the component's isDisposed field in long-running work is still a good habit.

Next step

To learn where and how services are obtained, go on to Resolving Services.