add: webseite mit jwt
This commit is contained in:
377
webseite-react-php-jwt/react-app/README.md
Normal file
377
webseite-react-php-jwt/react-app/README.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user