# Boundaries

Error- und Loading Boundaries sind ein zentrales Werkzeug, um Ladezustände
anzuzeigen und Fehler im UI gezielt abzufangen. Sie verhindern, dass Fehler
unkontrolliert die gesamte Anwendung zum Absturz bringen, und bieten die
Möglichkeit, Usern eine verständliche Rückmeldung zu geben. Gleichzeitig
unterstützen sie Entwicklern durch Logging und Monitoring bei der Fehleranalyse.

Da jede Anwendung unterschiedliche Anforderungen hat, gibt es kein universelles
Schema, wo und wie Boundaries am besten platziert werden. Die folgenden Best
practices sollen bei der Orientierung und Entscheidungsfindung helfen.

---

  Guideline für interne Teams

    Diese Guideline richtet sich in erster Linie an das interne Team von
    mittwald und setzt entsprechendes Vorwissen voraus. Die darin genannten
    Komponenten beziehen sich auf interne mStudio-Komponenten.

# Technische Umsetzung

Bei der Platzierung der Boundaries empfiehlt es sich, vom Groben ins Detail
vorzugehen. Entscheidend ist eine gute Balance: Zu viele kleine Boundaries
erhöhen die Komplexität, zu wenige gefährden hingegen die Stabilität großer
Teile der Anwendung.

Boundaries wie "LayoutCard" und "Section" rendern das jeweilige Element,
Boundaries mit dem Zusatz "Fragment" nutzen die Fehler und Ladezustände des
jeweiligen Elements, rendern am Ende aber nur ein Fragment.

| Typ                             | Verwendung                                                     | Fehlerfall                                                                             | Loading                                                                                      |
| ------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| LayoutCard / LayoutCardFragment | Äußerste Boundary einer Seite                                  | 

```tsx
import {
  AlertIcon,
  LayoutCard,
  Heading,
  Text,
  IllustratedMessage,
} from "@mittwald/flow-react-components";

<LayoutCard style={{ minWidth: 300 }}>
  <IllustratedMessage color="danger">
    <AlertIcon status="danger" />
    <Heading>Fehler beim Laden</Heading>
    <Text>
      Dieser Bereich konnte nicht geladen werden. Wir
      arbeiten daran das Problem zu beheben.
    </Text>
  </IllustratedMessage>
</LayoutCard>
```

 | 

```tsx
import {
  Heading,
  LayoutCard,
  Section,
  SkeletonText,
  Text,
} from "@mittwald/flow-react-components";

<LayoutCard>
  <Section>
    <Heading>
      <SkeletonText width="35%" />
    </Heading>
    <Text>
      <SkeletonText />
    </Text>
    <Text>
      <SkeletonText />
    </Text>
  </Section>
  <Section>
    <Heading>
      <SkeletonText width="35%" />
    </Heading>
    <Text>
      <SkeletonText />
    </Text>
    <Text>
      <SkeletonText />
    </Text>
  </Section>
</LayoutCard>
```

     |
| SectionsFragment                | Äußerste Boundary in Tabs                                      | 

```tsx
import {
  AlertIcon,
  Heading,
  IllustratedMessage,
  Text,
} from "@mittwald/flow-react-components";

<IllustratedMessage color="danger">
  <AlertIcon status="danger" />
  <Heading>Fehler beim Laden</Heading>
  <Text>
    Dieser Bereich konnte nicht geladen werden. Wir arbeiten
    daran das Problem zu beheben.
  </Text>
</IllustratedMessage>
```

        | 

```tsx
import {
  Heading,
  Section,
  SkeletonText,
  Text,
} from "@mittwald/flow-react-components";

<>
  <Section>
    <Heading>
      <SkeletonText width="35%" />
    </Heading>
    <Text>
      <SkeletonText />
    </Text>
    <Text>
      <SkeletonText />
    </Text>
  </Section>
  <Section>
    <Heading>
      <SkeletonText width="35%" />
    </Heading>
    <Text>
      <SkeletonText />
    </Text>
    <Text>
      <SkeletonText />
    </Text>
  </Section>
</>
```

            |
| Section                         | Ersetzt normale Sections                                       | 

```tsx
import {
  AlertIcon,
  Heading,
  Section,
  Text,
} from "@mittwald/flow-react-components";

<Section>
  <Heading color="danger">
    <AlertIcon status="danger" />
    Fehler beim Laden
  </Heading>
  <Text>
    Dieser Bereich konnte nicht geladen werden. Wir arbeiten
    daran das Problem zu beheben.
  </Text>
</Section>
```

                    | 

```tsx
import {
  Heading,
  Section,
  SkeletonText,
  Text,
} from "@mittwald/flow-react-components";

<Section>
  <Heading>
    <SkeletonText width="35%" />
  </Heading>
  <Text>
    <SkeletonText />
  </Text>
  <Text>
    <SkeletonText />
  </Text>
</Section>
```

                        |
| ContentFragment                 | Für mehrzeilige Inhalte                                        | 

```tsx
import {
  Alert,
  Heading,
  Text,
} from "@mittwald/flow-react-components";

<Alert status="danger">
  <Heading>Fehler beim Laden</Heading>
  <Text>
    Dieser Bereich konnte nicht geladen werden. Wir arbeiten
    daran das Problem zu beheben.
  </Text>
</Alert>
```

           | 

```tsx
import {
  Section,
  SkeletonText,
  Text,
} from "@mittwald/flow-react-components";

<Section>
  <Text>
    <SkeletonText />
  </Text>
  <Text>
    <SkeletonText />
  </Text>
  <Text>
    <SkeletonText />
  </Text>
</Section>
```

               |
| Fragment                        | Für Inhalte die im Lade / Fehlerzustand nicht angezeigt werden | Kein Fallback                                                                          | Kein Fallback                                                                                |
| Modal                           | Ersetzt normale Modals                                         | 

```tsx
import {
  ActionGroup,
  AlertIcon,
  Button,
  Heading,
  IllustratedMessage,
  Text,
} from "@mittwald/flow-react-components";

<div
  style={{
    background: "var(--overlay--background-color)",
    margin: "calc(var(--size-px--l) * -1)",
    padding: "var(--size-px--l)",
  }}
>
  <StaticModal>
    <div className="flow--modal--content">
      <IllustratedMessage color="danger">
        <AlertIcon status="danger" />
        <Heading>Fehler beim Laden</Heading>
        <Text>
          Dieser Bereich konnte nicht geladen werden. Wir
          arbeiten daran das Problem zu beheben.
        </Text>
      </IllustratedMessage>
    </div>
    <ActionGroup className="flow--modal--action-group">
      <Button color="secondary" variant="soft">
        Schließen
      </Button>
    </ActionGroup>
  </StaticModal>
</div>
```

                      | 

```tsx
import { LoadingSpinner } from "@mittwald/flow-react-components";

<div
  style={{
    background: "var(--overlay--background-color)",
    margin: "calc(var(--size-px--l) * -1)",
    padding: "var(--size-px--xxl)",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
  }}
>
  <LoadingSpinner color="dark" />
</div>
```

 (Loading View der Flow Component) |
| ContextMenu                     | Ersetzt normale ContextMenus                                   | 

```tsx
import { AlertText } from "@mittwald/flow-react-components";

<div
  style={{
    width: "fit-content",
    padding: "var(--popover--padding--s)",
    backgroundColor: "var(--popover--background-color)",
    boxShadow: "var(--popover--box-shadow)",
    borderRadius: "var(--popover--corner-radius)",
    borderWidth: "var(--popover--border-width)",
    borderStyle: "var(--popover--border-style)",
    borderColor: "var(--popover--border-color)",
  }}
>
  <div
    style={{
      padding:
        "var(--menu-item--padding-y) var(--menu-item--padding-x)",
    }}
  >
    <AlertText status="danger">Fehler beim Laden</AlertText>
  </div>
</div>
```

                          | 

```tsx
import { SkeletonText } from "@mittwald/flow-react-components";

<div
  style={{
    width: "fit-content",
    rowGap: "var(--menu--item-to-item-spacing)",
    padding: "var(--popover--padding--s)",
    flexDirection: "column",
    display: "flex",
    backgroundColor: "var(--popover--background-color)",
    boxShadow: "var(--popover--box-shadow)",
    borderRadius: "var(--popover--corner-radius)",
    borderWidth: "var(--popover--border-width)",
    borderStyle: "var(--popover--border-style)",
    borderColor: "var(--popover--border-color)",
    minWidth: "var(--popover--min-width)",
  }}
>
  <div
    style={{
      padding:
        "var(--menu-item--padding-y) var(--menu-item--padding-x)",
    }}
  >
    <SkeletonText />
  </div>
  <div
    style={{
      padding:
        "var(--menu-item--padding-y) var(--menu-item--padding-x)",
    }}
  >
    <SkeletonText />
  </div>
  <div
    style={{
      padding:
        "var(--menu-item--padding-y) var(--menu-item--padding-x)",
    }}
  >
    <SkeletonText />
  </div>
</div>
```

                              |
| MenuItemFragment                | Für MenuItems                                                  | Kein Fallback                                                                          | Keine Loading Boundary                                                                       |
| ChartFragment                   | Für CartesianCharts                                            | 

```tsx
import {
  AlertIcon,
  CartesianChart,
  Heading,
  IllustratedMessage,
  Text,
  XAxis,
  YAxis,
} from "@mittwald/flow-react-components";

<CartesianChart
  height="300px"
  emptyView={
    <IllustratedMessage color="danger">
      <AlertIcon status="danger" />
      <Heading>Fehler beim Laden</Heading>
      <Text>
        Dieser Bereich konnte nicht geladen werden. Wir
        arbeiten daran das Problem zu beheben.
      </Text>
    </IllustratedMessage>
  }
>
  <XAxis />
  <YAxis domain={[0, 100]} unit=" %" />
</CartesianChart>
```

                      | 

```tsx
import {
  CartesianChart,
  LoadingSpinner,
  XAxis,
  YAxis,
} from "@mittwald/flow-react-components";

<CartesianChart
  height="300px"
  emptyView={<LoadingSpinner />}
>
  <XAxis />
  <YAxis domain={[0, 100]} unit=" %" />
</CartesianChart>
```

                          |
| FieldFragment                   | Für Form Fields mit nachgeladenen Inhalten                     | 

```tsx
import { AlertText } from "@mittwald/flow-react-components";

<AlertText status="danger">Fehler beim Laden</AlertText>
```

                                  | 

```tsx
import {
  Label,
  SkeletonText,
  TextField,
} from "@mittwald/flow-react-components";

<TextField isReadOnly isRequired>
  <Label>
    <SkeletonText width="50%" />
  </Label>
</TextField>
```

                            |
| Avatar                          | Für Avatare                                                    | 

```tsx
import { Avatar } from "@mittwald/flow-react-components";

<Avatar />
```

                                | 

```tsx
import {
  Avatar,
  Skeleton,
} from "@mittwald/flow-react-components";

<Avatar>
  <Skeleton height="100%" />
</Avatar>
```

                                    |
| Text / String                   | Für Texte                                                      | 

```tsx
import { AlertText } from "@mittwald/flow-react-components";

<AlertText status="danger">Fehler beim Laden</AlertText>
```

                                  | 

```tsx
import {
  Text,
  SkeletonText,
} from "@mittwald/flow-react-components";

<Text>
  <SkeletonText />
</Text>
```

                                      |

## Inhalte einer LayoutCard

Sind Funktionen oder Informationen einer LayoutCard essenziell für die Nutzung
der Seite, sollte die gesamte LayoutCard mit `WithBoundaries.LayoutCard`
umschlossen sein. Dabei wird die Anzahl der geladenen Sections als
`sectionsCount` mitgegeben, um die Loading View passend darzustellen.

### Listenseite

Eine Listenseite enthält in der Regel eine einzelne Section. Der `sectionsCount`
beträgt hier daher 1. Der Count der Liste wird zuerst geladen, damit die gesamte
Liste im Fehlerfall in die ErrorView wechseln kann.

```
export const CronjobsPage: FC = () => (
  <WithBoundaries.LayoutCard suspenseFallbackProps={{ sectionsCount: 1 }}>
    {() => {
      const { projectId } = usePathParams("projectId");
      const projectGhost = ProjectGhost.ofId(projectId);
      const cronjobCount = projectGhost.cronjobs.getTotalCount().use();

      return (
        <>
          <ProjectDeactivatedAlert project={projectGhost} />
          <WithBoundaries.Section>
            {backupCount === 0 ? <CronjobIllustratedMessage project={projectGhost} /> : <CronjobList project={projectGhost} />}
          </WithBoundaries.Section>
        </>
      );
    }}
  </WithBoundaries.LayoutCard>
);
```

### Detailseite

Der `sectionsCount` sollte der erwarteten Anzahl an Sections entsprechen. So
bleibt das Layout während des Ladevorgangs stabil. Das Hauptelement der Seite
wird zuerst geladen, damit im Fehlerfall für die gesamte Seite eine ErrorView
angezeigt werden kann.

```
export const CronjobPage: FC = () => (
  <WithBoundaries.LayoutCard suspenseFallbackProps={{ sectionsCount: 2 }}>
    {() => {
      const { cronjobId } = usePathParams("cronjobId");
      const cronjob = CronjobGhost.ofId(cronjobId).getCommon().use();

      return (
        <>
          <ProjectDeactivatedAlert project={cronjob.project} />
          <GeneralSection cronjob={cronjob} />
          <IntervalSection cronjob={cronjob} />
        </>
      );
    }}
  </WithBoundaries.LayoutCard>
);
```

### Tab

Manche Seiten verteilen Inhalte auf mehrere Tabs. In solchen Fällen kann es
sinnvoll sein, Tabs separat abzusichern. Dafür eignet sich
`WithBoundaries.SectionsFragment`.

```
export const CronjobTabs: FC = () => (
  <LayoutCard>
    <Tabs>
      <Tab>
        <TabTitle>Allgemein</TabTitle>
        <WithBoundaries.SectionsFragment suspenseFallbackProps={{ sectionsCount: 3 }}>
          {() => {
            const { cronjobId } = usePathParams("cronjobId");
            const cronjob = CronjobGhost.ofId(cronjobId).getCommon().use();

            return (
              <>
                <ProjectDeactivatedAlert project={cronjob.project} />
                <GeneralSection cronjob={cronjob} />
                <IntervalSection cronjob={cronjob} />
              </>
            );
          }}
        </WithBoundaries.SectionsFragment>
      </Tab>
    </Tabs>
  </LayoutCard>
);
```

## Modals

Für Modals sollte immer ein `WithBoundaries.Modal` verwendet werden. Dies
verhindert auch das Nachladen von Daten innerhalb des Modals, bevor das Modal
geöffnet wird.

### Inhalte von Forms in Modals

Eine teilweise geladene oder fehlerhafte Form untergräbt das Vertrauen der User
und kann zu unvollständigen oder inkonsistenten Daten führen. Sobald innerhalb
der Form ein Fehler auftritt, fällt automatisch das ganze Modal in eine
ErrorView, auch wenn es darin noch Boundaries gibt.

```
interface Props {
  cronjob:CronjobGhost;
  controller?: OverlayController;
}

export const RenameCronjobModal: FC<Props> = (props) => (
  <WithBoundaries.Modal controller={props.controller}>
    {() => {
      const { cronjobGhost } = asGhostProps(props, ["cronjob"]);
      const controller = useModalController();
      const form = useForm();
      const handleOnSubmit = async () => {
        ...
        controller.close();
      };

      return (
        <Form form={form} onSubmit={handleOnSubmit}>
          ...
        </Form>
      );
    }}
  </WithBoundaries.Modal>
);
```

## Inhalte einzelner Sections

Eine Section kann ergänzende Informationen oder Funktionen aus einem anderen
Service enthalten, die das primäre Nutzererlebnis der Seite nicht einschränken.
In solchen Fällen lohnt sich eine Abgrenzung über `WithBoundaries.Section`. So
bleiben Fehler auf einen kleinen Bereich beschränkt, ohne die gesamte Seite zu
gefährden.

```
export const AppInstallationSection: FC<Props> = (props) => (
  <WithBoundaries.Section>
    {() => {
      const { cronjobGhost } = asGhostProps(props);
      const appInstallation = cronjobGhost.getCommon().linkedAppInstallation.getCommon().use();
      const t = useTCronjob();

      return (
        <>...</>
      );
    }}
  </WithBoundaries.Section>
);
```

**Sonderfall:** Manche Sections werden nur unter bestimmten Bedingungen
angezeigt. In diesen Situationen soll keine eigene Loading View erzeugt werden,
damit Nutzer kein Ladeverhalten sehen, das später nicht zu einem sichtbaren
Ergebnis führt. Für solche Fälle eignet sich `WithBoundaries.Fragment`, da es
die Section schützt, ohne einen sichtbaren Ladezustand zu erzeugen.

```
export const AppInstallationSection: FC<Props> = (props) => (
  <WithBoundaries.Fragment>
    {() => {
      const { cronjobGhost } = asGhostProps(props);
      const cronjob = cronjobGhost.getCommon().use();
      const t = useTCronjob();

      if(!cronjob.linkedAppInstallation){
        return null;
      }

      return (
        <Section>...</Section>
      );
    }}
  </WithBoundaries.Fragment>
);
```

## Inhalte eines Diagramms

Diagramme beziehen in der Regel viele Daten aus externen Services und sind daher
fehleranfällig. Sie sollten immer von einer Boundary umschlossen werden. Für das
`CartesianChart` gibt es hierfür das `WithBoundaries.ChartFragment`.

```
<WithBoundaries.ChartFragment>
  <CartesianChart>...</CartesianChart>
</WithBoundaries.ChartFragment>
```

## Inhalte einzelner Elemente

Kleine Elemente wie Texte, Links, Badges, Actions oder ContextMenus werden oft
dynamisch geladen, da sie das Nutzungserlebnis der Seite lediglich ergänzen.
Diese sollten daher separat geschützt werden, um nicht die gesamte Seite zu
blockieren. Diese Elemente besitzen oft keine eigene ErrorView und erscheinen im
Fehlerfall nicht oder verbleiben in ihrem Ladezustand.

### Texte

Für Texte gibt es die `WithBoundaries.Text` Component, bzw die
`WithBoundaries.String` Component, je nachdem ob am Ende eine `Text` Component
oder nur ein String gerendert werden soll.

```
<LabeledValue>
  <Label>{t("projectShortId")}</Label>
  <WithBoundaries.Text>
    {() =>  projectGhost.getCommon().use().shortId }
  </WithBoundaries.Text>
</LabeledValue>
```

Zusätzlich gibt es Components, die direkt auf ein Model angewendet werden können
und eine eingebaute Boundary besitzen.

```
<ModelLink model={cronjob}>
  <ModelTitle model={cronjob} />
</ModelLink>

<ModelLabeledValue model={cronjob}/>
```

### Alerts / Badges

Alerts und Badges ergänzen eine Seite oft nur um kleine Hinweise oder
Statusinformationen. Sie werden in ein `WithBoundaries.Fragment` verpackt. So
bleiben sie geschützt, ohne eine eigene Loading View auszulösen.

```
export const CustomerBankruptAlert: FC<Props> = (props) => (
  <WithBoundaries.Fragment>
    {() => {
      const { customerGhost, asBadge } = asGhostProps(props, ["customer"]);
      const isBankrupt = customerGhost.getCommon().isBankrupt().use();

      if (!isBankrupt) {
        return null;
      }

      if (asBadge) {
        return <AlertBadge>...</AlertBadge>
      }

      return <Alert>...</Alert>;
    }}
  </WithBoundaries.Fragment>
);
```

### Field

Für Fields kann die `WithBoundaries.FieldFragment` Component verwendet werden.
Sie zeigt im Loading State ein Textfeld, dem optional ein Label mitgegeben
werden kann.

```
export const AppInstallationSelectField: FC<Props> = (props) => {
  const { name, label, projectGhost } = asGhostProps(props, ["project"]);

  return (
    <WithBoundaries.FieldFragment suspenseFallbackProps={{ label }}>
      {() => {
        const appInstallations = projectGhost.appInstallations.execute().use().items;

        return (
          <Field name={name}>
            <Select isDisabled={appInstallations.length === 0}>
              <Label>{label}</Label>
              {sortedAppInstallations.map((i) => (
                <Option key={i.id} value={i.id}>
                  {i.description}
                </Option>
              ))}
            </Select>
          </Field>
        );
      }}
    </WithBoundaries.FieldFragment>
  );
};
```

### ContextMenu

Für ContextMenus wird `WithBoundaries.ContextMenu` verwendet. Um zu verhindern,
dass der Inhalt des ContextMenus springt, werden Elemente die nachgeladene
Inhalte benötigen in ein `WithBoundaries.MenuItem` gepackt, diese Boundary
enthält kein Suspense, damit das ganze ContextMenu im Ladezustand verbleibt.

```
export const ActionsContextMenu: FC<Props> = (props) => {
  const { appInstallationGhost } = asGhostProps(props);
  const deleteModalController = useModalController();
  const updateModalController = useModalController();

  return (
    <>
      <WithBoundaries.ContextMenu placement="bottom end">
        {() => {
          const { isBusy } = appInstallationGhost.getCommon().use();
          return (
            <>
              <DeleteMenuItem onAction={() => deleteModalController.open()} isDisabled={isBusy} />
              <WithBoundaries.MenuItemFragment>
                {() => {
                  const updateAvailable = appInstallationGhost.getCommon().appVersion.updateAvailable().use();
                  if (!updateAvailable) {
                    return null;
                  }
                  return (
                    <UpdateMenuItem onAction={() => updateModalController.open()}/>
                  );
                }}
              </WithBoundaries.MenuItemFragment>
            <>
          )
        }}
      </WithBoundaries.ContextMenu>

      <DeleteCronjobModal controller={deleteModalController} appInstallation={appInstallationGhost}/>
      <UpdateCronjobModal controller={updateModalController} appInstallation={appInstallationGhost}/>
    </>
  );
};
```

---

# Weitere Empfehlungen

Ein durchdachtes Loading und ErrorBoundary Konzept endet nicht bei der
technischen Umsetzung. Fehler sollten zuverlässig erfasst und an
Monitoring-Systeme, Sentry oder Datadog weitergegeben werden, damit Probleme
früh sichtbar werden. Manuelle und automatisierte Tests helfen dabei, die
eigenen Fallbacks regelmäßig zu überprüfen und sicherzustellen, dass sie in
allen relevanten Situationen greifen. Ebenso wichtig ist eine klare und
konsistente Formulierung der Fehlermeldungen, damit User verstehen, was passiert
und wie sie weitermachen können. Für konkrete Hinweise zur sprachlichen
Ausgestaltung bietet die Guideline
[Fehlermeldungen](/02-foundations/03-content-guidelines/03-fehlermeldungen)
weitere Orientierung. So entsteht ein stabiles und nachvollziehbares Verhalten,
das sowohl die Entwicklung als auch die Nutzung der Anwendung unterstützt.
