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.
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.
| Typ | Verwendung | Element | Fehlerfall | Loading |
|---|---|---|---|---|
LayoutCard | Äußerste Boundary einer Seite | LayoutCard | LayoutCard mit IllustratedMessage | LayoutCard mit Sections mit mehrzeiligen Skeletons |
LayoutCardFragment | Für Dashboards mit ungewisser Anzahl und Breite der Kacheln | Fragment | LayoutCard mit IllustratedMessage | LayoutCard mit Sections mit mehrzeiligen Skeletons |
SectionsFragment | Äußerste Boundary in Tabs | Fragment | IllustratedMessage | Sections mit mehrzeiligen Skeletons |
Section | Ersetzt normale Sections | Section | Fehler Section | Mehrzeilige Skeletons |
ContentFragment | Für mehrzeilige Inhalte | Fragment | Fehler Section | Mehrzeilige Skeletons |
Fragment | Für Inhalte die im Lade / Fehlerzustand nicht angezeigt werden | Fragment | Nichts | Nichts |
Modal | Ersetzt normale Modals | Modal | IllustratedMessage | LoadingView der Flow Component |
ContextMenu | Ersetzt normale ContextMenus | ContextMenu | MenuItems mit Fehler Text | MenuItems mit Skeleton |
ChartFragment | Für CartesianCharts | Fragment | CartesianChart mit IllustratedMessage | CartesianChart mit LoadingSpinner |
FieldFragment | Für Form Fields mit nachgeladenen Inhalten | Fragment | Fehler Text | TextField |
Avatar | Für Avatare | Avatar | Avatar mit Fehler Icon | Avatar mit Skeleton |
Link | Für Links | Link | Fehler Text | Einzeiliges Skeleton |
Text | Für Texte | Text | Fehler Text | Einzeiliges Skeleton |
String | Für einzeilige Inhalte | Fragment | Fehler Text | Einzeiliges Skeleton |
Inhalte einer LayoutCard (WithBoundaries.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 LoadingView 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 (WithBoundaries.SectionsFragment)
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 (WithBoundaries.Modal)
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:CronjobProxy;
controller?: OverlayController;
}
export const RenameCronjobModal: FC<Props> = (props) => (
<WithBoundaries.Modal controller={props.controller}>
{() => {
const { cronjobProxy } = asProxyProps(props, ["cronjob"]);
const controller = useOverlayController("Modal");
const form = useForm();
const handleOnSubmit = async () => {
...
controller.close();
};
return (
<Form form={form} onSubmit={handleOnSubmit}>
...
</Form>
);
}}
</WithBoundaries.Modal>
);
Inhalte einzelner Sections (WithBoundaries.Section)
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 LoadingView 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 (WithBoundaries.ChartFragment)
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 kleinerer 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 (WithBoundaries.Text)
<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 (WithBoundaries.Fragment)
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 LoadingView auszulösen.
export const CustomerBankruptAlert: FC<Props> = (props) => (
<WithBoundaries.Fragment>
{() => {
const { customerProxy, asBadge } = asProxyProps(props, ["customer"]);
const isBankrupt = customerProxy.getDetailed().isBankrupt().use();
if (!isBankrupt) {
return null;
}
if (asBadge) {
return <AlertBadge>...</AlertBadge>
}
return <Alert>...</Alert>;
}}
</WithBoundaries.Fragment>
);
Field (WithBoundaries.FieldFragment)
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 (WithBoundaries.ContextMenu)
export const ActionsContextMenu: FC<Props> = (props) => {
const { cronjobProxy } = asProxyProps(props);
const deleteModalController = useOverlayController("Modal");
return (
<>
<WithBoundaries.ContextMenu placement="bottom end">
{() => {
const cronjob = cronjobProxy.getCommon().use();
return <DeleteMenuItem onAction={() => deleteModalController.open()} />;
}}
</WithBoundaries.ContextMenu>
<DeleteCronjobModal
controller={deleteModalController}
cronjob={cronjobProxy}
/>
</>
);
};
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 weitere Orientierung. So entsteht ein stabiles und nachvollziehbares Verhalten, das sowohl die Entwicklung als auch die Nutzung der Anwendung unterstützt.