React-Komponenten sind keine HTML-Templates. Sie sind JavaScript-Funktionen, die UI beschreiben. Dieses Kapitel erklärt, was Komponenten sind, wie sie Daten über Props empfangen, und wie man sie zu komplexen Anwendungen zusammensetzt.
Das TSX-Kapitel hat die Syntax erklärt – wie man TSX schreibt, wie man Props typisiert, wie Conditional Rendering funktioniert. Dieses Kapitel geht tiefer: Was macht eine gute Komponente aus? Wie organisiert man Komponenten-Hierarchien? Wie designed man wiederverwendbare Komponenten?
Eine React-Komponente ist eine Funktion, die UI beschreibt. Nicht mehr, nicht weniger.
function Greeting() {
return <h1>Hello World</h1>;
}
Das ist eine gültige Komponente. Sie nimmt keine Eingabe, produziert
immer die gleiche Ausgabe. Wie eine mathematische Funktion:
f() = "Hello World".
Aber UI ohne Daten ist nutzlos. Jede interessante Komponente braucht Eingaben. In React heißen diese Eingaben Props.
function Greeting(props: { name: string }) {
return <h1>Hello {props.name}</h1>;
}
// Verwendung
<Greeting name="Alice" /> // → <h1>Hello Alice</h1>
<Greeting name="Bob" /> // → <h1>Hello Bob</h1>
Jetzt ist Greeting eine echte Funktion:
f(name) = <h1>Hello {name}</h1>. Gleiche
Eingabe, gleiche Ausgabe. Deterministisch, vorhersagbar, testbar.
Props sind Parameter. Sie machen Komponenten konfigurierbar, wiederverwendbar, komponierbar.
Einfache Props:
interface ButtonProps {
text: string;
variant: 'primary' | 'secondary';
}
function Button({ text, variant }: ButtonProps) {
return <button className={`btn btn-${variant}`}>{text}</button>;
}
// Verschiedene Konfigurationen, gleiche Komponente
<Button text="Save" variant="primary" />
<Button text="Cancel" variant="secondary" />
Props sind Read-Only:
Komponenten dürfen ihre Props nie ändern. Props sind Eingaben, keine Variablen.
// ❌ NIEMALS
function Counter({ count }: { count: number }) {
count = count + 1; // FEHLER: Props sind read-only
return <div>{count}</div>;
}
// ✓ Props nur lesen, nicht ändern
function Counter({ count }: { count: number }) {
return <div>{count}</div>;
}
Props fließen von oben nach unten. Parent → Child. Niemals umgekehrt. Das ist der unidirektionale Datenfluss.
Warum unidirektional?
Vorhersagbarkeit. Wenn ein Wert falsch ist, schauen Sie nach oben – zum Parent. Nicht seitwärts, nicht nach unten. Linear, einfach, debuggbar.
function App() {
const user = { name: 'Alice', role: 'admin' };
return <Dashboard user={user} />;
}
function Dashboard({ user }: { user: User }) {
return <Header userName={user.name} />;
}
function Header({ userName }: { userName: string }) {
return <h1>Welcome, {userName}</h1>;
}
Datenfluss: App → Dashboard →
Header. Klar, nachvollziehbar, kein Rätsel.
Props können alles sein, was JavaScript ausdrücken kann.
Primitive Typen:
<Component
text="Hello" // String
count={42} // Number
isActive={true} // Boolean
factor={3.14} // Number
/>
Objekte:
interface User {
id: number;
name: string;
email: string;
}
function UserCard({ user }: { user: User }) {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
const alice = { id: 1, name: 'Alice', email: 'alice@example.com' };
<UserCard user={alice} />
Arrays:
function TagList({ tags }: { tags: string[] }) {
return (
<div>
{tags.map(tag => <span key={tag}>#{tag}</span>)}
</div>
);
}
<TagList tags={['react', 'typescript', 'web']} />
Funktionen:
interface ButtonProps {
text: string;
onClick: () => void;
}
function Button({ text, onClick }: ButtonProps) {
return <button onClick={onClick}>{text}</button>;
}
<Button text="Click" onClick={() => alert('Clicked!')} />
Funktionen als Props sind der Mechanismus für Child → Parent Kommunikation. Das Kind ruft die Funktion auf, der Parent reagiert.
React-Elemente (children):
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="content">{children}</div>
</div>
);
}
<Card title="My Card">
<p>This is card content</p>
<button>Action</button>
</Card>
children ist eine spezielle Prop. Alles zwischen
öffnendem und schließendem Tag wird als children
übergeben.
Nicht alle Props müssen immer gesetzt sein.
interface ButtonProps {
text: string; // Required
variant?: 'primary' | 'secondary'; // Optional
disabled?: boolean; // Optional
onClick?: () => void; // Optional
}
function Button({
text,
variant = 'primary', // Default-Wert
disabled = false,
onClick
}: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{text}
</button>
);
}
// Alle gültig
<Button text="Save" />
<Button text="Delete" variant="secondary" />
<Button text="Submit" disabled={true} onClick={handleSubmit} />
Das ? markiert Props als optional. Default-Werte in der
Destructuring-Syntax setzen Fallbacks.
Komponenten bauen auf Komponenten auf. Kleine Komponenten werden zu größeren zusammengesetzt.
// Atomare Komponente: Button
function Button({ text, onClick }: ButtonProps) {
return <button onClick={onClick}>{text}</button>;
}
// Molekül: Form mit Buttons
function LoginForm({ onSubmit }: { onSubmit: (data: LoginData) => void }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = () => {
onSubmit({ email, password });
};
return (
<form>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<Button text="Login" onClick={handleSubmit} />
</form>
);
}
// Organismus: Page mit Form
function LoginPage() {
const handleLogin = (data: LoginData) => {
// Login-Logik
};
return (
<div className="page">
<h1>Login</h1>
<LoginForm onSubmit={handleLogin} />
</div>
);
}
Atomic Design-Terminologie: Atoms → Molecules → Organisms → Templates → Pages.
Aber das Prinzip ist universell: Klein anfangen, nach oben komponieren.
Gute Komponenten sind wiederverwendbar. Schlechte Komponenten sind zu spezifisch.
Schlecht: Zu spezifisch
function UserProfileButton() {
return (
<button className="btn btn-primary" onClick={() => navigate('/profile')}>
View Profile
</button>
);
}
Nur für genau diesen Use-Case. Kann nirgendwo anders verwendet werden.
Besser: Konfigurierbar
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary';
onClick: () => void;
}
function Button({ text, variant = 'primary', onClick }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{text}
</button>
);
}
// Jetzt vielseitig verwendbar
<Button text="View Profile" onClick={() => navigate('/profile')} />
<Button text="Logout" variant="secondary" onClick={handleLogout} />
<Button text="Delete" variant="danger" onClick={handleDelete} />
Prinzip: Mache Komponenten so generisch wie nötig, so spezifisch wie möglich.
Zu generisch → unnötig komplex. Zu spezifisch → nicht wiederverwendbar. Balance finden.
Wenn Props durch viele Ebenen gereicht werden müssen, wird es unübersichtlich.
function App() {
const user = { name: 'Alice', role: 'admin' };
return <Page user={user} />;
}
function Page({ user }: { user: User }) {
return <Layout user={user} />;
}
function Layout({ user }: { user: User }) {
return <Header user={user} />;
}
function Header({ user }: { user: User }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }: { user: User }) {
return <div>Welcome, {user.name}</div>;
}
user wird durch 4 Ebenen gereicht, obwohl nur
UserMenu es braucht. Das ist Prop
Drilling.
Lösungen: 1. Komponenten-Struktur überdenken 2. Context API verwenden (späteres Kapitel) 3. State Management (Redux, Zustand)
Prop Drilling ist nicht immer schlecht. Für 1-2 Ebenen ist es OK. Ab 3+ Ebenen wird es problematisch.
1. Container/Presenter Pattern
Trennung von Logik (Container) und UI (Presenter).
// Presenter: Pure UI, keine Logik
interface UserListProps {
users: User[];
onUserClick: (id: number) => void;
}
function UserList({ users, onUserClick }: UserListProps) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
// Container: Logik, API-Calls, State
function UserListContainer() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
const handleUserClick = (id: number) => {
navigate(`/user/${id}`);
};
return <UserList users={users} onUserClick={handleUserClick} />;
}
Presenter ist rein, testbar, wiederverwendbar. Container hat State, Effects, Business-Logik.
2. Compound Components
Komponenten, die zusammenarbeiten, aber einzeln verwendet werden können.
function Tabs({ children }: { children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="tabs">
{React.Children.map(children, (child, index) =>
React.cloneElement(child as React.ReactElement, {
isActive: index === activeTab,
onClick: () => setActiveTab(index)
})
)}
</div>
);
}
function Tab({ title, children, isActive, onClick }: TabProps) {
return (
<div className={isActive ? 'tab active' : 'tab'} onClick={onClick}>
<h3>{title}</h3>
{isActive && <div>{children}</div>}
</div>
);
}
// Verwendung
<Tabs>
<Tab title="Home">Home Content</Tab>
<Tab title="Profile">Profile Content</Tab>
<Tab title="Settings">Settings Content</Tab>
</Tabs>
3. Render Props
Komponente erhält eine Funktion als Prop, die UI rendert.
interface MouseTrackerProps {
render: (position: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return <div>{render(position)}</div>;
}
// Verwendung
<MouseTracker
render={({ x, y }) => (
<h1>Mouse at {x}, {y}</h1>
)}
/>
Heute meist durch Custom Hooks ersetzt, aber das Pattern ist wichtig zu kennen.
// Types
interface Product {
id: number;
name: string;
price: number;
image: string;
inStock: boolean;
}
// Atomare Komponente: Badge
interface BadgeProps {
text: string;
variant: 'success' | 'warning' | 'danger';
}
function Badge({ text, variant }: BadgeProps) {
return <span className={`badge badge-${variant}`}>{text}</span>;
}
// Atomare Komponente: Price
function Price({ amount }: { amount: number }) {
return <span className="price">${amount.toFixed(2)}</span>;
}
// Molekül: ProductCard
interface ProductCardProps {
product: Product;
onAddToCart: (id: number) => void;
}
function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<div className="details">
<h3>{product.name}</h3>
<Price amount={product.price} />
{product.inStock ? (
<Badge text="In Stock" variant="success" />
) : (
<Badge text="Out of Stock" variant="danger" />
)}
</div>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
>
Add to Cart
</button>
</div>
);
}
// Organismus: ProductGrid
interface ProductGridProps {
products: Product[];
onAddToCart: (id: number) => void;
}
function ProductGrid({ products, onAddToCart }: ProductGridProps) {
return (
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
}
// Page: ProductPage
function ProductPage() {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
fetchProducts().then(setProducts);
}, []);
const handleAddToCart = (id: number) => {
// Add to cart logic
console.log('Added product', id);
};
return (
<div className="page">
<h1>Products</h1>
<ProductGrid products={products} onAddToCart={handleAddToCart} />
</div>
);
}
Diese Hierarchie demonstriert: - Kleine, fokussierte Komponenten
(Badge, Price) - Zusammengesetzte Komponenten
(ProductCard) - Listen-Komponenten
(ProductGrid) - Container-Komponenten
(ProductPage) - Props-Flow von oben nach unten -
Callback-Props für Events nach oben
1. Ein Concern pro Komponente
// ❌ Zu viel in einer Komponente
function UserDashboard() {
// Fetch users
// Fetch orders
// Render user profile
// Render order history
// Render analytics
// ...
}
// ✓ Aufteilen
function UserDashboard() {
return (
<>
<UserProfile />
<OrderHistory />
<Analytics />
</>
);
}
2. Props-Interface benennen
// ✓ Konsistente Benennung
interface ButtonProps { /* ... */ }
function Button(props: ButtonProps) { /* ... */ }
interface UserCardProps { /* ... */ }
function UserCard(props: UserCardProps) { /* ... */ }
3. Destructuring in Signatur
// ✓ Cleaner, klarer
function Button({ text, variant, onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{text}</button>;
}
// ❌ Verbose
function Button(props: ButtonProps) {
return <button className={props.variant} onClick={props.onClick}>{props.text}</button>;
}
4. Komponenten klein halten
Faustregel: Wenn eine Komponente >100 Zeilen hat, überlegen Sie, ob sie aufgeteilt werden kann.
5. TypeScript nutzen
Immer Props-Interfaces definieren. TypeScript verhindert Bugs zur Build-Zeit.
Komponenten und Props sind das Fundament von React. Komponenten kapseln UI und Logik. Props machen sie konfigurierbar. Komposition erlaubt komplexe Anwendungen aus einfachen Bausteinen. Das ist das React-Modell – einfach, mächtig, skalierbar.