6 Komponenten und Props – Das React-Komponentenmodell

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?

6.1 Komponenten: UI als Funktionen

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.

6.2 Props: Die Schnittstelle zwischen Komponenten

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: AppDashboardHeader. Klar, nachvollziehbar, kein Rätsel.

6.3 Prop-Typen: Mehr als Strings

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.

6.4 Optionale Props und Defaults

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.

6.5 Komponenten-Hierarchien und Komposition

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.

6.6 Wiederverwendbarkeit durch Props-Design

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.

6.7 Prop Drilling: Das Problem

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.

6.8 Component Patterns

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.

6.9 Ein vollständiges Beispiel: Product Card

// 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

6.10 Best Practices

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.