React + Vite Development Skill
Dieser Skill hilft bei der Entwicklung von React-Anwendungen mit Vite Build-System. Er ist optimiert für deine Projekte: digitalTwin, DresdenAIInsights, und ManufacturingInsideAnalyzer.
Wann wird dieser Skill aktiviert?
- Du arbeitest mit
.tsx-Dateien incomponents/odersrc/ - Du erwähnst Keywords wie: component, React, Vite, useState, useEffect
- Du möchtest UI-Komponenten erstellen oder debuggen
Kern-Prinzipien
1. Komponenten-Struktur
Single Responsibility: Jede Komponente hat EINEN klaren Zweck.
Beispiel - GUT:
// components/UserProfile.tsx
export function UserProfile({ userId }: { userId: string }) {
const { user, loading } = useUser(userId);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
Beispiel - SCHLECHT:
// Macht zu viel: Fetching + Rendering + Validation
export function UserProfile() {
const [user, setUser] = useState();
useEffect(() => { fetch(...) }, []);
const validate = () => { ... };
return <div>...</div>;
}
2. Props über Drilling
Problem: Props durch 5+ Komponenten durchreichen Lösung: Context API oder Custom Hooks
Beispiel:
// hooks/useTheme.ts
export function useTheme() {
return useContext(ThemeContext);
}
// components/Button.tsx
export function Button() {
const { theme } = useTheme(); // Kein Props-Drilling!
return <button className={theme}>Click</button>;
}
3. Performance-Optimierung
Nur wenn nötig memoizen:
useMemo: Teure Berechnungen (Filter, Transformationen)useCallback: Callbacks in dep-ArraysReact.memo: Komponenten mit vielen Re-Renders
Beispiel:
function DataTable({ items }: { items: Item[] }) {
// GUT: Teurer Filter wird gecached
const filtered = useMemo(
() => items.filter(i => i.score > 0.5),
[items]
);
return <Table data={filtered} />;
}
4. TypeScript-First
Alle Props typisieren:
interface ButtonProps {
variant: 'primary' | 'secondary';
onClick: () => void;
children: React.ReactNode;
disabled?: boolean;
}
export function Button({ variant, onClick, children, disabled }: ButtonProps) {
return <button
}
5. Error Boundaries
Jede kritische Komponente braucht Fehlerbehandlung:
<ErrorBoundary fallback={<ErrorMessage />}>
<CriticalComponent />
</ErrorBoundary>
Projektspezifische Guidelines
digitalTwin (React 19 + Gemini AI)
- AI-Responses immer validieren: Gemini-Output kann fehlerhaft sein
- Streaming UI: Zeige Loading-States bei AI-Calls
- Camera-Permissions: Immer Fallback für denied permissions
- Mock-Daten: Dev-Mode sollte ohne Camera funktionieren
Beispiel:
function CameraView() {
const [stream, setStream] = useState<MediaStream | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
navigator.mediaDevices.getUserMedia({ video: true })
.then(setStream)
.catch(() => setError('Camera access denied'));
}, []);
if (error) return <MockCameraView />;
return <video srcObject={stream} />;
}
DresdenAIInsights (React 18.3 + Radix UI)
- 40+ Radix UI Components: Nutze vorhandene Primitives
- Accessibility: ARIA-Labels für alle interaktiven Elemente
- Particle Effects: Performance-Monitor bei Animationen
- IONOS-Deployment: Statische Builds nur (kein SSR)
Beispiel Radix:
import * as Dialog from '@radix-ui/react-dialog';
function ContactModal() {
return (
<Dialog.Root>
<Dialog.Trigger>Kontakt</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Kontaktformular</Dialog.Title>
{/* Content */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
ManufacturingInsideAnalyzer (React 19 + Vercel)
- DSGVO-Compliance: Siehe DSGVO-Skill
- Smart Sampling: UI muss große Datasets (1567×591) handhaben
- Vercel Edge Functions: API-Calls via
/api/Routes - 10-Sekunden-Timeout: Zeige Progress bei langen Analysen
Beispiel:
function AnalysisProgress({ fileSize }: { fileSize: number }) {
const [progress, setProgress] = useState(0);
const timeoutMs = 10000;
useEffect(() => {
const interval = setInterval(() => {
setProgress(p => Math.min(p + 10, 95));
}, timeoutMs / 10);
return () => clearInterval(interval);
}, []);
return <ProgressBar value={progress} max={100} />;
}
Vite-spezifische Best Practices
1. Environment Variables
Nur VITE_-Prefix wird exposed:
// .env
VITE_API_URL=https://api.example.com
SECRET_KEY=xxx // ❌ Wird NICHT in Bundle inkludiert
// code
const apiUrl = import.meta.env.VITE_API_URL; // ✅
2. Code Splitting
Route-basiertes Lazy Loading:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
3. Asset Handling
Bilder optimieren:
// ❌ Nicht optimal
<img src="/logo.png" />
// ✅ Vite optimiert automatisch
import logoUrl from './assets/logo.png';
<img src={logoUrl} alt="Logo" />
Testing
Jede Komponente braucht mindestens:
- Smoke Test (rendert ohne Fehler)
- User Interaction Test (Clicks, Inputs)
- Edge Case Test (Leere Daten, Fehler)
Beispiel mit Vitest:
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('renders button', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
test('calls onClick', async () => {
const
render(<Button
await userEvent.click(screen.getByText('Click'));
expect(onClick).toHaveBeenCalledOnce();
});
Deployment-Checklist
Vor jedem Deployment:
-
npm run builderfolgreich - TypeScript Errors:
npx tsc --noEmit - Keine Console.logs im Production-Code
- Environment Variables korrekt gesetzt
- Asset-Pfade relativ (kein
C:\...) - Bundle Size < 500KB (gzipped)
Ressourcen
Für tiefere Themen siehe:
- styling.md - TailwindCSS + CSS-in-JS
- state-management.md - Context vs. Zustand
- performance.md - Profiling + Optimization
- accessibility.md - ARIA + Keyboard Navigation
Layout Anti-Patterns (KRITISCH - Lesson Learned 2025-11-18)
Problem: Überschüssige Nested Divs
Root Cause: Unbewusste Verschachtelung führt zu Layout-Problemen und schwer debugbaren Style-Konflikten.
Goldene Regel: Maximal 2 nested divs pro logische UI-Section.
SCHLECHT:
// ❌ 5 Ebenen verschachtelt - unnötig komplex
function LoginGate({ children }: { children: React.ReactNode }) {
return (
<div className="outer-wrapper">
<div className="container">
<div className="inner-container">
<div className="content-wrapper">
<div className="content">
{children}
</div>
</div>
</div>
</div>
</div>
);
}
GUT:
// ✅ Maximal 2 Ebenen - einfach und debuggbar
function LoginGate({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-slate-900 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{children}
</div>
</div>
);
}
Warum?
- Debugging: Weniger Ebenen → einfacher zu inspizieren
- Performance: Weniger DOM-Nodes → schneller Rendering
- Maintainability: Klare Struktur → leichter zu verstehen
- CSS Specificity: Weniger Verschachtelung → weniger Konflikte
Inspection Technique (Chrome DevTools):
# Open DevTools → Elements Tab
# Click component → Check nesting depth
# If >2 levels of plain divs (no semantic tags) → REFACTOR!
Problem: Conflicting Styles durch Layout-Ebenen
Symptom: Styles werden überschrieben, obwohl Tailwind-Classes korrekt sind.
Root Cause: Übergeordnete Wrapper haben konkurrierende Layout-Properties.
Beispiel - Conflict:
// ❌ Parent hat flex, Child hat grid → Konflikt!
function App() {
return (
<div className="flex flex-col min-h-screen"> {/* flex container */}
<main className="grid grid-cols-2 gap-4"> {/* grid versucht sich durchzusetzen */}
{/* Layout bricht! */}
</main>
</div>
);
}
// ✅ Konsistentes Layout-System
function App() {
return (
<div className="flex flex-col min-h-screen">
<main className="flex-1 p-4"> {/* flex child */}
<div className="grid grid-cols-2 gap-4"> {/* grid INNERHALB von flex */}
{/* Layout funktioniert! */}
</div>
</main>
</div>
);
}
Debugging Checklist:
- Prüfe Parent-Layout (flex vs grid vs block)
- Nur EINE Layout-Strategie pro Hierarchie-Ebene
- Verwende semantische Tags (
<main>,<section>,<article>) statt generischer<div> - Tailwind DevTools: Inspect applied classes
Problem: CSP (Content Security Policy) Violations
Symptom: Production-Build funktioniert, aber Styles fehlen oder werden blockiert.
Root Cause: External CDNs (z.B. Tailwind CDN) werden von CSP blockiert.
SCHLECHT:
<!-- ❌ NIEMALS in Production! -->
<head>
<script src="https://cdn.tailwindcss.com"></script>
</head>
GUT:
# ✅ Lokale Installation
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { extend: {} },
plugins: [],
}
# postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
CSP Header Verification:
# Check production CSP
curl -I https://your-production-url.com | grep -i content-security-policy
# Expected (without CDN):
# Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
Pre-Commit Hook:
# .husky/pre-commit
#!/bin/sh
# Check for CDN links in HTML/TSX files
if grep -r "cdn\." src/ index.html 2>/dev/null; then
echo "❌ ERROR: External CDN detected!"
echo "Remove CDN links and use local packages instead."
exit 1
fi
Regel:
- Nie externe CDNs in Production (Tailwind, jQuery, Bootstrap)
- Immer lokale npm packages verwenden
- CSP-Header testen nach jedem Deployment
- Pre-commit hooks für CDN-Detection
Theme Switching Patterns (Dark Mode)
Implementation mit Context + localStorage
Pattern: Theme-State in Context, Persistence in localStorage
// contexts/ThemeContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
// Read from localStorage on mount
const stored = localStorage.getItem('theme');
return (stored === 'dark' || stored === 'light') ? stored : 'light';
});
useEffect(() => {
// Persist to localStorage on change
localStorage.setItem('theme', theme);
// Update document class for Tailwind dark: selector
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
Usage in Components:
// components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
className="p-2 rounded-lg bg-slate-200 dark:bg-slate-700"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
Tailwind Config:
// tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class', // ✅ Use 'dark' class on <html>
theme: {
extend: {
colors: {
// Custom dark mode colors
dark: {
bg: '#0f172a', // slate-900
surface: '#1e293b', // slate-800
border: '#334155', // slate-700
}
}
}
}
}
CSS Variables Alternative (für komplexe Themes):
/* index.css */
:root {
--color-bg: #ffffff;
--color-text: #000000;
--color-primary: #3b82f6;
}
:root.dark {
--color-bg: #0f172a;
--color-text: #f1f5f9;
--color-primary: #60a5fa;
}
/* Usage in components */
.button {
background-color: var(--color-primary);
color: var(--color-text);
}
System Preference Detection:
// Detect OS dark mode preference
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem('theme');
if (stored) return stored as Theme;
// Fallback to system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});
// Listen to system preference changes
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
Regel:
- localStorage für User-Präferenz (überschreibt System)
- System Preference als Fallback
- document.documentElement.classList für Tailwind
dark:selector - CSS Variables für komplexe Theme-Systeme mit vielen Farben
Design System Components (Accessibility-First)
Dropdown mit Accessibility
Problem: White text on white background (unreadable)
Root Cause: Browser default styles für <option> ignorieren color property.
SCHLECHT:
// ❌ Options erben NICHT parent color/background!
<select className="bg-slate-800 text-white">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
// Resultat: Weißer Text auf weißem Hintergrund im Dropdown!
GUT:
// ✅ Explizite Styles auf <option> Elementen
<select className="bg-slate-800 text-white border border-slate-600 rounded px-3 py-2">
<option value="1" className="bg-slate-800 text-white">Option 1</option>
<option value="2" className="bg-slate-800 text-white">Option 2</option>
<option value="3" className="bg-slate-800 text-white">Option 3</option>
</select>
Best Practice - Reusable Component:
// components/Select.tsx
interface SelectOption {
value: string;
label: string;
}
interface SelectProps {
options: SelectOption[];
value: string;
onChange: (value: string) => void;
label: string;
disabled?: boolean;
}
export function Select({ options, value, onChange, label, disabled }: SelectProps) {
return (
<div className="flex flex-col gap-2">
<label
htmlFor={label}
className="text-sm font-medium text-slate-200"
>
{label}
</label>
<select
id={label}
value={value}
=> onChange(e.target.value)}
disabled={disabled}
className="bg-slate-800 text-white border border-slate-600 rounded px-3 py-2
focus:outline-none focus:ring-2 focus:ring-blue-500
disabled:opacity-50 disabled:cursor-not-allowed"
aria-label={label}
>
{options.map(({ value, label }) => (
<option
key={value}
value={value}
className="bg-slate-800 text-white" // ✅ CRITICAL!
>
{label}
</option>
))}
</select>
</div>
);
}
Usage:
<Select
label="Analysis Type"
value={selectedType}
options={[
{ value: 'quality', label: 'Quality Analysis' },
{ value: 'maintenance', label: 'Predictive Maintenance' },
{ value: 'performance', label: 'Performance Optimization' },
]}
/>
Accessibility Checklist:
-
<label>mitfor/htmlForverknüpft -
aria-labelfür Screen Readers -
disabledstate mit opacity + cursor -
focus:ringfür Keyboard-Navigation sichtbar - Explizite
classNameauf JEDEM<option>
File Upload Component
Pattern: Drag & Drop + File Input mit Preview
// components/FileUpload.tsx
import { useState, useRef } from 'react';
interface FileUploadProps {
accept: string;
maxSizeMB: number;
onFileSelected: (file: File) => void;
}
export function FileUpload({ accept, maxSizeMB, onFileSelected }: FileUploadProps) {
const [dragActive, setDragActive] = useState(false);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleFile = (file: File) => {
// Validate file size
const maxSizeBytes = maxSizeMB * 1024 * 1024;
if (file.size > maxSizeBytes) {
setError(`File too large. Max size: ${maxSizeMB}MB`);
return;
}
// Validate file type
const fileExtension = file.name.split('.').pop()?.toLowerCase();
const acceptedExtensions = accept.split(',').map(ext => ext.trim().replace('.', ''));
if (!fileExtension || !acceptedExtensions.includes(fileExtension)) {
setError(`Invalid file type. Accepted: ${accept}`);
return;
}
setError(null);
onFileSelected(file);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) handleFile(file);
};
return (
<div className="w-full">
<div
className={`
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
transition-colors duration-200
${dragActive ? 'border-blue-500 bg-blue-50' : 'border-slate-600 bg-slate-800'}
hover:border-blue-400
`}
=> setDragActive(true)}
=> setDragActive(false)}
=> e.preventDefault()}
=> inputRef.current?.click()}
>
<input
ref={inputRef}
type="file"
accept={accept}
className="hidden"
aria-label="File upload"
/>
<div className="flex flex-col items-center gap-2">
<svg className="w-12 h-12 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-slate-200">
Drag & drop file here, or <span className="text-blue-400 underline">browse</span>
</p>
<p className="text-sm text-slate-400">
Accepted: {accept} (Max {maxSizeMB}MB)
</p>
</div>
</div>
{error && (
<p className="mt-2 text-sm text-red-400" role="alert">
{error}
</p>
)}
</div>
);
}
Usage:
<FileUpload
accept=".csv,.xlsx,.json"
maxSizeMB={5}
=> {
console.log('Selected file:', file.name);
// Process file...
}}
/>
Progress Indicator (Long-Running Operations)
Pattern: Fake progress with realistic timing
// components/AnalysisProgress.tsx
import { useState, useEffect } from 'react';
interface AnalysisProgressProps {
timeoutMs: number; // Expected completion time (e.g., 10000)
onComplete?: () => void;
}
export function AnalysisProgress({ timeoutMs, onComplete }: AnalysisProgressProps) {
const [progress, setProgress] = useState(0);
useEffect(() => {
// Incremental progress updates (10 steps)
const interval = setInterval(() => {
setProgress(prev => {
const next = prev + 10;
if (next >= 100) {
clearInterval(interval);
onComplete?.();
return 100;
}
return next;
});
}, timeoutMs / 10);
return () => clearInterval(interval);
}, [timeoutMs, onComplete]);
return (
<div className="w-full">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-400">Analyzing data...</span>
<span className="text-sm text-slate-200">{progress}%</span>
</div>
<div className="w-full h-2 bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
{progress >= 95 && (
<p className="mt-2 text-sm text-yellow-400">
Finalizing analysis...
</p>
)}
</div>
);
}
Usage with API Call:
function AnalysisView() {
const [analyzing, setAnalyzing] = useState(false);
const [result, setResult] = useState<string | null>(null);
const handleAnalyze = async (data: any) => {
setAnalyzing(true);
try {
const response = await fetch('/api/analyze', {
method: 'POST',
body: JSON.stringify(data),
});
const result = await response.json();
setResult(result.analysis);
} catch (error) {
console.error('Analysis failed:', error);
} finally {
setAnalyzing(false);
}
};
if (analyzing) {
return <AnalysisProgress timeoutMs={10000} />;
}
if (result) {
return <div>{result}</div>;
}
return <button => handleAnalyze(data)}>Analyze</button>;
}
Regel:
- Fake progress ist OK für UX (besser als Spinner)
- 95% cap bis API tatsächlich fertig (verhindert 100% ohne Ergebnis)
- aria- attributes* für Accessibility
- Optimistic UI: Zeige Fortschritt, auch wenn Backend-Timing variiert
Support
Bei Fragen zu spezifischen Patterns:
- Prüfe project-spezifische
AGENTS.md - Siehe vorhandene Komponenten als Beispiele
- Frage nach Code-Review via Agent
Lesson Learned Origins
Layout Anti-Patterns: Incident 2025-11-18 (Nested divs caused style conflicts) CSP Violations: Incident 2025-11-18 (Tailwind CDN blocked in production) Dropdown Accessibility: Bug 2025-11-19 (White text on white background) Theme Switching: Feature 2025-11-12 (OAuth login with dark mode)