# 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 (
{children}
);
};
```
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 (
);
};
```
`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.