perf(dashboard): React-Review - Memoization, stabile Callbacks, Key-Fixes
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user