Files
JS/webseite-react-php-jwt/react-app/README.md
2026-09-01 12:05:06 +02:00

377 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Installation der Pakete
## Dependencies
```bash
npm install react-router-dom bootstrap uuid axios react-icons
```
## DevDependencies
```bash
npm install sass -D
```
## Configuration von Vite
```js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
css: {
preprocessorOptions: {
scss: {
quietDeps: true, // Unix
silenceDeprecations: [
'color-functions',
'global-builtin',
'import',
'legacy-js-api',
],
},
},
},
});
```
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
# Products Store mit useContext und useReducer
`TableProducts` hält den Tabellen-State nicht mehr lokal in der Komponente. Stattdessen gibt es einen ausgelagerten Store aus **Context + Reducer**. So können Kind-Komponenten wie `RowProduct` den State lesen und Aktionen auslösen, ohne Callbacks per Props durchzureichen (kein Prop Drilling).
## Dateistruktur
```
src/stores/products/
initialState.js // Startzustand (Factory)
reducer.js // reine State-Übergänge
ProductsStore.jsx // Context, Provider, Custom Hooks
index.js // öffentliche Exports
```
## Step 1: `initialState` auslagern
Datei: `src/stores/products/initialState.js`
Der Startzustand wird als **Factory-Funktion** geschrieben, nicht als festes Objekt. So kann der Provider die geladenen Produkte (`items`) übergeben, und Validierung bleibt an einer Stelle.
```js
export const createInitialState = (items) => {
return { items: items || [] };
};
```
Später wird sie so verwendet:
```js
useReducer(productsReducer, items, createInitialState);
```
React ruft `createInitialState(items)` einmal beim ersten Render auf (lazy init). Das dritte Argument von `useReducer` ist genau dafür da.
## Step 2: Reducer auslagern
Datei: `src/stores/products/reducer.js`
Der Reducer bleibt eine **reine Funktion**: `(state, action) => newState`. Keine Side Effects außer einem `console.warn` für unbekannte Actions.
Action-Typen liegen in einem Objekt `PRODUCTS_ACTIONS`. Tippfehler fallen so früher auf als bei losen Strings wie `'UPDATE_AMMOUNT'`.
```js
export const PRODUCTS_ACTIONS = {
UPDATE_AMOUNT: 'UPDATE_AMOUNT',
UPDATE_PRICE: 'UPDATE_PRICE',
};
```
Jede Action folgt dem Muster `{ type, payload }`. Der Reducer kopiert den State (`...state`) und ersetzt nur das betroffene Item über `map`. Unveränderte Items behalten dieselbe Referenz das ist wichtig für `memo(RowProduct)`.
## Step 3: Context aufteilen (State und Dispatch)
Datei: `src/stores/products/ProductsStore.jsx`
Best Practice: **zwei Contexts**, nicht einen.
| Context | Inhalt | Wann re-rendern Kind-Komponenten? |
| ------------------------- | ----------- | ------------------------------------- |
| `ProductsStateContext` | `{ items }` | bei jeder State-Änderung |
| `ProductsDispatchContext` | `dispatch` | praktisch nie (`dispatch` ist stabil) |
`RowProduct` braucht nur Aktionen (also `dispatch`). Würde State und Dispatch in einem Value `{ state, dispatch }` stecken, bekäme jede Zeile bei jeder Eingabe ein neues Objekt und würde neu rendern `memo` wäre wirkungslos.
```js
const ProductsStateContext = createContext(null);
const ProductsDispatchContext = createContext(null);
```
`null` als Default ist Absicht: so erkennt der Custom Hook, wenn jemand den Store **außerhalb** des Providers nutzt.
## Step 4: Provider mit `useReducer` bauen
`ProductsProvider` ist die einzige Stelle, an der `useReducer` läuft. Er bekommt `items` (z. B. aus dem Fetch auf der Page) und reicht State sowie `dispatch` nach unten.
```jsx
export const ProductsProvider = (props) => {
const { children, items = [] } = props;
const [state, dispatch] = useReducer(
productsReducer,
items,
createInitialState,
);
return (
<ProductsStateContext.Provider value={state}>
<ProductsDispatchContext.Provider value={dispatch}>
{children}
</ProductsDispatchContext.Provider>
</ProductsStateContext.Provider>
);
};
```
Wichtig: Die Komponente, die `useContext` aufruft, muss ein **Kind** des Providers sein nicht dieselbe Komponente, die den Provider rendert. Deshalb gibt es `TableProducts` (Provider) und `TableProductsView` (Consumer).
## Step 5: Custom Hooks statt nacktem `useContext`
Drei Hooks kapseln den Zugriff und werfen einen Fehler, wenn der Provider fehlt:
1. `useProductsState()` liest `{ items }`
2. `useProductsDispatch()` liefert `dispatch`
3. `useProductsActions()` fertige Callbacks `updateAmount` / `updatePrice`
Komponenten sollen nicht `dispatch({ type: '…' })` selbst zusammenbauen. Die Action-Objekte gehören in den Store, analog zu Redux Action Creators.
```js
const updateAmount = useCallback(
(amount, id) => {
dispatch({ type: PRODUCTS_ACTIONS.UPDATE_AMOUNT, payload: { id, amount } });
},
[dispatch],
);
```
`useCallback` + stabiles `dispatch` hält die Funktionsreferenz gleich. Zusammen mit `memo(RowProduct)` rendern nur Zeilen neu, deren Item-Props sich geändert haben.
## Step 6: Barrel-Export
Datei: `src/stores/products/index.js`
Andere Dateien importieren nur den Ordner, nicht interne Pfade:
```js
import {
ProductsProvider,
useProductsState,
useProductsActions,
} from '../../stores/products';
```
Reducer und `initialState` bleiben intern austauschbar, ohne dass `TableProducts` oder `RowProduct` ihre Imports ändern müssen.
## Step 7: `TableProducts` an den Store anbinden
`TableProducts` wrappt die Ansicht mit dem Provider und übergibt die Props als Initial-Items:
```jsx
const TableProducts = (props) => {
return (
<ProductsProvider items={props.items}>
<TableProductsView />
</ProductsProvider>
);
};
```
`TableProductsView` liest den State und berechnet den Gesamtpreis lokal mit `useMemo`. Der Gesamtpreis ist **abgeleiteter State** er gehört nicht in den Reducer, sondern wird aus `items` berechnet.
Die Handler `onHandleAmount` / `onHandlePrice` entfallen. `RowProduct` holt sie selbst aus dem Store.
## Step 8: Prop Drilling in `RowProduct` entfernen
Vorher: `TableProducts``RowProduct` → Inputs (Callbacks als Props).
Nachher: `RowProduct` ruft `useProductsActions()` auf. Die Zeile bekommt nur noch die Produktdaten als Props (`_id`, `title`, `stock`, `price`, `sku`). Die `_id` bindet den Input-Wert an das richtige Item:
```js
const { updateAmount, updatePrice } = useProductsActions();
const handleAmount = useCallback(
(value) => {
updateAmount(value, _id);
},
[updateAmount, _id],
);
```
Die Produktdaten bleiben Props (nicht aus dem Context gelesen). Würde jede Zeile `items` aus dem State-Context lesen, würden **alle** Zeilen bei jeder Änderung neu rendern.
## Datenfluss
```
TableProductsPage fetch JSON → items
TableProducts ProductsProvider(items)
├── TableProductsView useProductsState() → Tabelle + Total
└── RowProduct useProductsActions() → UPDATE_AMOUNT / UPDATE_PRICE
productsReducer neuer items-Array → Context → UI
```
## Kurz: warum diese Aufteilung?
- **`initialState.js`** Startzustand und Validierung, unabhängig von React
- **`reducer.js`** alle State-Übergänge testbar ohne Komponenten
- **`ProductsStore.jsx`** React-Anbindung (Context, Provider, Hooks)
- **zwei Contexts** Zeilen, die nur dispatchen, re-rendern nicht unnötig
- **Custom Hooks** klare API, Fehler wenn der Provider fehlt
- **kein Prop Drilling** neue Kind-Komponenten können den Store direkt nutzen
---
# Architektur & Funktionsweise des ProductsProvider
Dieser Store implementiert ein **Optimistic UI mit entkoppeltem, gepuffertem Backend-Sync (Debounce & Merge)**.
```
UI Input (z. B. Tippen im Input-Feld)
├──▶ 1. dispatch(...) ──▶ State aktualisiert sich SOFORT (UI reagiert flüssig)
└──▶ 2. schedulePatch(...) ──▶ Änderungen sammeln & Timer starten (400 ms)
(nach 400 ms Ruhe)
flushPatch(...) ──▶ HTTP PATCH an API
```
---
## 1. Kernkonzept: `schedulePatch` vs. `flushPatch`
Beim Bearbeiten von Feldern (wie Preis oder Menge) feuert die Eingabe bei jedem Tastendruck (`onChange`). Würde man jedes Mal sofort einen API-Request senden, würde das Backend mit HTTP-Aufrufen überflutet werden.
### `schedulePatch(id, patch)` Das Sammelbecken & der Timer
**Zweck:** Nimmt Änderungen entgegen, führt sie pro Produkt zusammen (_mergen_) und setzt den Countdown zurück (_debouncen_).
1. **Merge (`pendingPatches`):**
- Tippt der Nutzer schnell hintereinander Menge `5` und Preis `19.99`, merkt sich die Map: `{ stock: 5, price: 19.99 }`.
2. **Timer-Reset (`persistTimers`):**
- Läuft bereits ein Timer für dieses Produkt, wird er mit `clearTimeout` gestoppt.
- Ein neuer 400ms-Timer startet. Erst wenn der Nutzer **400 ms lang nichts mehr tippt**, läuft der Timer ab.
---
### `flushPatch(id)` Der Ausführer
**Zweck:** Holt die gesammelten Änderungen ab und sendet den eigentlichen API-Request.
1. **Puffer leeren:** Holt das zusammengefasste Patch-Objekt aus `pendingPatches.current` und löscht es sofort aus der Map (verhindert doppelte Requests).
2. **Timer aufräumen:** Entfernt eventuell noch vorhandene Timer-Referenzen.
3. **API-Call (`patchProduct`):** Sendet den minimalen Patch an den Server.
4. **Erfolg:** Aktualisiert das Produkt im State mit den offiziellen Serverdaten (`SET_ITEM`).
5. **Fehlerfall (Rollback):** Schlägt der Request fehl, wird der Fehler gesetzt und via `loadProducts()` der alte/gültige Serverzustand nachgeladen (Rollback des optimistischen UI-Updates).
---
## 2. Der Unmount-Cleanup (`useEffect`)
Verlässt der Nutzer die Seite oder wird der Provider abgebaut, greift die Cleanup-Funktion:
```javascript
useEffect(() => {
return () => {
// 1. Laufende Timer sofort stoppen
timers.forEach((timer) => clearTimeout(timer));
timers.clear();
// 2. Ungespeicherte Änderungen sofort absenden ("Flush on Unmount")
patches.forEach((patch, id) => {
if (patch && Object.keys(patch).length > 0) {
patchProduct(id, patch).catch(() => {});
}
});
patches.clear();
};
}, []);
```
- Verhindert Datenverlust, wenn der Nutzer kurz nach einer Eingabe navigiert.
---
## 3. Die 3-Context-Architektur
Der Store teilt die Verantwortlichkeiten in **drei separate Contexte** auf, um unnötige Re-Renders im Komponentenbaum vollständig zu unterbinden:
| Context | Hook | Inhalt | Re-Render Verhalten |
| ----------------------------- | ----------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **`ProductsStateContext`** | `useProductsState()` | `state` (Items, Loading, Error) | Rendert **nur** neu, wenn sich der State ändert (z. B. Listen-Komponenten). |
| **`ProductsDispatchContext`** | `useProductsDispatch()` | `dispatch` | **Rendert nie neu** (Referenz bleibt über gesamten App-Lebenszyklus stabil). |
| **`ProductsActionsContext`** | `useProductsActions()` | Methoden (`updatePrice`, `addProduct`, etc.) | **Rendert nie neu**, da alle Actions mit `useCallback` und stabilen Abhängigkeiten memoisiert sind. |
---
## 4. Übersicht der Action-Methoden
- **`updateAmount(amount, id)` & `updatePrice(price, id)`:**
- **Optimistisch & Debounced:** Ändern sofort den lokalen State via `dispatch` und reichen die Daten an `schedulePatch` weiter.
- **`addProduct(product)`, `saveProduct(id, product)`, `removeProduct(id)`:**
- **Pessimistisch & Direkt:** Warten direkt auf die API-Antwort (`async/await`) und updaten erst nach erfolgreichem Server-Response den Reducer-State.
- **`loadProducts()`:**
- Lädt die komplette Produktliste manuell neu (z. B. nach einem Rollback oder Pull-to-Refresh).
---
## useLayoutEffect vs. useEffect
Der Hauptunterschied liegt im **Timing der Ausführung** relativ zum Browser-Rendering:
| Eigenschaft | `useLayoutEffect` | `useEffect` |
| --- | --- | --- |
| **Ausführungsart** | **Synchron** (blockiert das Painting) | **Asynchron / Deferiert** (nach dem Painting) |
| **Zeitpunkt** | Direkt nach den DOM-Mutationen, *bevor* der Browser den Bildschirm aktualisiert | Nachdem der Browser die Pixel auf den Bildschirm gezeichnet hat |
| **Haupt-Use-Case** | DOM-Messungen (`getBoundingClientRect`), visuelle DOM-Mutationen zur Vermeidung von Layout Shifts / Flackern | Data Fetching, Subscriptions, Logging, Storage-Zugriffe |
| **Performance-Impact** | Kann UI-Ruckler verursachen, wenn Berechnungen zu lange dauern | Keine Blockierung der UI-Darstellung |
---
**Analyse deines Codes:**
* **`useLayoutEffect` für `setAuthToken(state.token)**`:
* *Zweck hier:* Der Token (z. B. in Axios-Headern oder API-Clients) wird **synchron** gesetzt, noch bevor Kindkomponenten in ihren eigenen `useEffect`-Hooks initiale API-Aufrufe ausführen. Dadurch wird ein Race Condition vermieden, bei dem ein Kind-Effekt einen Request ohne gültigen Header losschickt.
* **`useEffect` für `persistAuth(...)**`:
* *Zweck hier:* Das Schreiben in den `localStorage`/`IndexedDB` ist ein Side Effect, der die visuelle Darstellung nicht direkt beeinflusst und daher die UI nicht blockieren muss.