perf(dashboard): React-Review - Memoization, stabile Callbacks, Key-Fixes
CI / backend-tests (push) Failing after 2m4s
CI / frontend-build (push) Successful in 20s

React.memo auf allen 8 Kachel-Komponenten + TileFrame, damit Interaktionen (Anpassen/Monitor/Größe/Ausblenden) nicht mehr alle Kacheln unnötig neu rendern. Voraussetzung dafür: onAusblendenUmschalten/onGroesseAendern als stabile useCallback-Referenzen mit id-Parameter statt pro Kachel neu erzeugter Closures. Die vier Layout-Mutationsfunktionen (dragEnde/ausblendenUmschalten/groesseAendern/layoutZuruecksetzen) über eine gemeinsame aktualisiereLayout-Helper-Funktion mit funktionalem setState dedupliziert (vermeidet stale-closure-Risiko bei schnellen Interaktionen). Instabile Index-Keys in PrueftermineTile/AufgabenTile durch stabile Keys ersetzt. maengelOffenListe in MaengelTile mit useMemo. Fehler-Anzeige bekommt "Erneut versuchen"-Button. Drag-Griff in TileFrame: type="button" + aria-roledescription für Screenreader.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-07 13:27:36 +02:00
co-authored by Claude Sonnet 5
parent 726d6e29b1
commit 0c448c7ff7
11 changed files with 223 additions and 52 deletions
@@ -1,3 +1,4 @@
import { memo } from "react";
import { Link } from "react-router-dom";
import { InventoryStatus } from "../../../components/status/InventoryStatus";
@@ -7,7 +8,7 @@ interface Props {
ablaufdaten: Ablaufdatumeintrag[];
}
export function AblaufdatenTile({ ablaufdaten }: Props) {
function AblaufdatenTileKomponente({ ablaufdaten }: Props) {
if (ablaufdaten.length === 0) {
return <p className="text-muted">Keine bald ablaufenden Chargen.</p>;
}
@@ -26,3 +27,5 @@ export function AblaufdatenTile({ ablaufdaten }: Props) {
</ul>
);
}
export const AblaufdatenTile = memo(AblaufdatenTileKomponente);
@@ -1,3 +1,4 @@
import { memo } from "react";
import { Link } from "react-router-dom";
import type { HistorieEintrag } from "../../../api/types";
@@ -7,7 +8,7 @@ interface Props {
aktivitaet: HistorieEintrag[];
}
export function AktivitaetTile({ aktivitaet }: Props) {
function AktivitaetTileKomponente({ aktivitaet }: Props) {
if (aktivitaet.length === 0) {
return <p className="text-muted">Noch keine Ereignisse.</p>;
}
@@ -32,3 +33,5 @@ export function AktivitaetTile({ aktivitaet }: Props) {
</ul>
);
}
export const AktivitaetTile = memo(AktivitaetTileKomponente);
@@ -1,3 +1,4 @@
import { memo } from "react";
import { Link } from "react-router-dom";
import type { Einsatzbereitschaft, Kennzahlen } from "../types";
@@ -11,7 +12,7 @@ interface Props {
bereitschaft: Einsatzbereitschaft;
}
export function AufgabenTile({
function AufgabenTileKomponente({
kennzahlen,
maengelNeuAnzahl,
maengelKritischAnzahl,
@@ -50,9 +51,11 @@ export function AufgabenTile({
return (
<ul className="card-list">
{aufgaben.map((a, i) => (
<li key={i}>{a.ziel ? <Link to={a.ziel}>{a.text}</Link> : <span>{a.text}</span>}</li>
{aufgaben.map((a) => (
<li key={a.text}>{a.ziel ? <Link to={a.ziel}>{a.text}</Link> : <span>{a.text}</span>}</li>
))}
</ul>
);
}
export const AufgabenTile = memo(AufgabenTileKomponente);
@@ -1,4 +1,4 @@
import { useState } from "react";
import { memo, useState } from "react";
import { Link } from "react-router-dom";
import { GRUND_TEXT } from "../../../components/status/gruendeText";
@@ -18,7 +18,7 @@ interface Props {
bereitschaft: Einsatzbereitschaft;
}
export function EinsatzbereitschaftTile({ bereitschaft }: Props) {
function EinsatzbereitschaftTileKomponente({ bereitschaft }: Props) {
const [detailsOffen, setDetailsOffen] = useState(false);
return (
@@ -72,3 +72,5 @@ export function EinsatzbereitschaftTile({ bereitschaft }: Props) {
</>
);
}
export const EinsatzbereitschaftTile = memo(EinsatzbereitschaftTileKomponente);
@@ -1,10 +1,12 @@
import { memo } from "react";
import type { Kennzahlen } from "../types";
interface Props {
kennzahlen: Kennzahlen;
}
export function KennzahlenTile({ kennzahlen }: Props) {
function KennzahlenTileKomponente({ kennzahlen }: Props) {
return (
<div>
<p className="text-4xl font-bold tabular-nums leading-none">{kennzahlen.anzahl_offener_fehlbestaende}</p>
@@ -20,3 +22,5 @@ export function KennzahlenTile({ kennzahlen }: Props) {
</div>
);
}
export const KennzahlenTile = memo(KennzahlenTileKomponente);
@@ -1,4 +1,4 @@
import { useState } from "react";
import { memo, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import type { Mangel } from "../../../api/types";
@@ -9,9 +9,9 @@ interface Props {
maengelKritischAnzahl: number;
}
export function MaengelTile({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }: Props) {
function MaengelTileKomponente({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }: Props) {
const [maengelOffen, setMaengelOffen] = useState(false);
const maengelOffenListe = maengel.filter((m) => m.status !== "erledigt");
const maengelOffenListe = useMemo(() => maengel.filter((m) => m.status !== "erledigt"), [maengel]);
return (
<>
@@ -53,3 +53,5 @@ export function MaengelTile({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }
</>
);
}
export const MaengelTile = memo(MaengelTileKomponente);
@@ -1,4 +1,5 @@
import { Link } from "react-router-dom";
import { memo } from "react";
import { InspectionStatus } from "../../../components/status/InspectionStatus";
import type { Prueftermineintrag } from "../types";
@@ -7,15 +8,15 @@ interface Props {
prueftermine: Prueftermineintrag[];
}
export function PrueftermineTile({ prueftermine }: Props) {
function PrueftermineTileKomponente({ prueftermine }: Props) {
if (prueftermine.length === 0) {
return <p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>;
}
return (
<ul className="card-list">
{prueftermine.map((p, i) => (
<li key={i} className="row-between" style={{ padding: "0.3rem 0" }}>
{prueftermine.map((p) => (
<li key={`${p.typ}-${p.objekt_id}-${p.faelligkeitsdatum}`} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>
<Link to={`/akte/objekt/${p.objekt_id}`}>{p.bezeichnung}</Link>{" "}
<span className="text-muted">({p.faelligkeitsdatum})</span>
@@ -26,3 +27,5 @@ export function PrueftermineTile({ prueftermine }: Props) {
</ul>
);
}
export const PrueftermineTile = memo(PrueftermineTileKomponente);
@@ -1,10 +1,12 @@
import { memo } from "react";
import type { Qualifikationsablauf } from "../types";
interface Props {
qualifikationsablaeufe: Qualifikationsablauf[];
}
export function QualifikationTile({ qualifikationsablaeufe }: Props) {
function QualifikationTileKomponente({ qualifikationsablaeufe }: Props) {
if (qualifikationsablaeufe.length === 0) {
return <p className="text-muted">Keine bald ablaufenden Qualifikationen.</p>;
}
@@ -24,3 +26,5 @@ export function QualifikationTile({ qualifikationsablaeufe }: Props) {
</ul>
);
}
export const QualifikationTile = memo(QualifikationTileKomponente);