# 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.