Generische React-Komponenten schreiben
AI generated
<T>
type
TypeScript · React · Generics · Komponenten
Generische React-Komponenten schreiben
Ein Typparameter statt zehn fast identischer Komponenten

Eine Select-Komponente für Produkte, eine zweite für Nutzer, eine dritte für Kategorien, jede fast identisch, nur mit anderem Datentyp: Genau dieses Muster lösen generische React-Komponenten. Statt für jeden Datentyp eine eigene Komponente zu duplizieren, wird der Typ selbst zum Parameter, und der Compiler prüft bei jeder Verwendung, ob Daten, Callback und Rendering zueinander passen. Dieser Artikel zeigt, wie generische React-Komponenten in der Praxis entstehen, inklusive JSX-Syntax, Constraints und forwardRef.

14 Min. Lesezeit Generic Components · Constraints · forwardRef TypeScript 5.x · React 18/19

1. Das Problem: fast identische Komponenten je Datentyp

In vielen React-Codebasen entstehen im Laufe der Zeit mehrere Komponenten, die strukturell identisch sind und sich nur im verarbeiteten Datentyp unterscheiden: eine ProductSelect, eine UserSelect, eine CategorySelect, jeweils mit derselben Logik für Auswahl, Filterung und Rendering. Der einzige Unterschied liegt im konkreten Typ der Optionen und im Callback, der beim Auswählen ausgelöst wird. Diese Duplizierung widerspricht dem Grundprinzip, Logik nur einmal zu implementieren, und erzeugt bei jeder Änderung an der gemeinsamen Logik mehrere Stellen, die synchron gehalten werden müssen.

Generische React-Komponenten lösen dieses Problem auf genau die Art, wie es Generics auch bei normalen Funktionen tun: Der konkrete Datentyp wird durch einen Typparameter ersetzt, den der Compiler bei jeder Verwendung mit dem tatsächlichen Typ füllt. Eine einzige Select<T>-Komponente ersetzt dann ProductSelect, UserSelect und CategorySelect gleichzeitig, ohne dass die Typsicherheit an irgendeiner Stelle verloren geht. Der Compiler kennt bei jeder konkreten Verwendung exakt, welcher Typ hinter T steckt, und prüft Props, Callback-Parameter und Rendering-Funktion entsprechend.

Der Umstieg von mehreren spezifischen Komponenten auf eine generische Komponente lohnt sich immer dann, wenn dieselbe Struktur bereits zweimal für unterschiedliche Typen existiert oder absehbar ein drittes Mal gebraucht wird. Für eine Komponente, die dauerhaft nur mit einem einzigen konkreten Typ arbeitet, ist eine generische Lösung dagegen unnötige Komplexität, ein Punkt, auf den der Vergleichsabschnitt später genauer eingeht.

2. Grundsyntax: Typparameter an einer Funktionskomponente

Die Grundsyntax für generische React-Komponenten unterscheidet sich kaum von generischen Funktionen: Der Typparameter steht in spitzen Klammern direkt nach dem Funktionsnamen, gefolgt von der normalen Parameterliste mit dem Props-Objekt. Wichtig ist, dass die Komponente als benannte Funktion, nicht als Arrow-Function-Konstante deklariert wird, wenn sie generisch sein soll, weil die Kurzsyntax für generische Arrow Functions in .tsx-Dateien mit der JSX-Syntax für Tags kollidiert und deshalb eine Sonderschreibweise mit Komma nach dem Typparameter benötigt.

Bei einer generischen Komponente mit dem Namen List und dem Typparameter T beschreibt T den Typ der einzelnen Listenelemente. Innerhalb der Komponente steht T dann für jede Verwendung von Props, Zustandsvariablen oder Rückgabewerten zur Verfügung, exakt wie ein konkreter Typ, nur eben erst bei der Verwendung der Komponente durch den Compiler festgelegt.


// Named function declaration works cleanly with a type parameter in .tsx
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
  keyExtractor: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Arrow function form needs a trailing comma to avoid JSX ambiguity in .tsx
const List2 = <T,>({ items, renderItem, keyExtractor }: ListProps<T>) => {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
};

3. Type Inference in JSX: Warum die spitze Klammer manchmal stört

Bei der Verwendung einer generischen React-Komponente in JSX leitet der Compiler den konkreten Typ in den meisten Fällen automatisch aus der übergebenen items-Prop ab, ganz ohne dass der Typparameter explizit angegeben werden muss. Übergibt man ein Array vom Typ Product[], wird T automatisch zu Product, und alle weiteren Props wie renderItem und keyExtractor werden gegen diesen abgeleiteten Typ geprüft. Diese automatische Ableitung ist der Regelfall und macht generische Komponenten in der Praxis genauso bequem zu verwenden wie nicht-generische.

Nur in seltenen Fällen, etwa wenn items ein leeres Array ist und der Compiler keinen konkreten Typ ableiten kann, muss der Typparameter explizit angegeben werden. In JSX geschieht das mit der Syntax <List<Product> items={[]} .../>. Diese explizite Form ist selten nötig, aber gut zu kennen, weil die Fehlermeldung ohne sie manchmal wenig aufschlussreich ist und fälschlich auf eine andere Stelle im Code verweist.


interface Product { id: number; name: string; price: number; }

const products: Product[] = [
  { id: 1, name: "Tastatur", price: 79 },
  { id: 2, name: "Monitor", price: 249 },
];

// T is inferred as Product from the "items" array, no explicit type argument needed
function ProductGrid() {
  return (
    <List
      items={products}
      renderItem={(product) => <span>{product.name}</span>}
      keyExtractor={(product) => product.id}
    />
  );
}

// Explicit type argument only needed when inference has nothing to work with
function EmptyGrid() {
  return (
    <List<Product>
      items={[]}
      renderItem={(product) => <span>{product.name}</span>}
      keyExtractor={(product) => product.id}
    />
  );
}

4. Generic Constraints: Typparameter sinnvoll einschränken

Nicht jeder Typparameter soll uneingeschränkt jeden beliebigen Typ zulassen. Eine generische Tabellen-Komponente etwa benötigt zwingend, dass jedes Element ein id-Feld für den React-key besitzt, ansonsten lässt sich die Komponente nicht sicher implementieren. Mit T extends { id: string | number } wird genau diese Anforderung als Constraint formuliert: Der Compiler akzeptiert nur Typen, die mindestens dieses Feld besitzen, und lehnt jeden Typ ohne id bereits beim Kompilieren ab, lange bevor die Komponente überhaupt gerendert wird.

Constraints machen generische React-Komponenten robuster, weil sie explizit dokumentieren, welche Mindeststruktur ein Datentyp erfüllen muss, um mit der Komponente kompatibel zu sein. Ohne Constraint müsste die Komponente entweder auf any zurückfallen oder mit einem zusätzlichen, separaten keyExtractor-Callback arbeiten, was zwar auch funktioniert, aber eine zusätzliche Prop und damit zusätzliche Komplexität an jeder Aufrufstelle bedeutet.


// Constraint: T must at least have an "id" field for the React key
interface DataTableProps<T extends { id: string | number }> {
  rows: T[];
  columns: Array<{ header: string; render: (row: T) => ReactNode }>;
}

function DataTable<T extends { id: string | number }>({ rows, columns }: DataTableProps<T>) {
  return (
    <table>
      <thead>
        <tr>{columns.map((col) => <th key={col.header}>{col.header}</th>)}</tr>
      </thead>
      <tbody>
        {rows.map((row) => (
          <tr key={row.id}>
            {columns.map((col) => <td key={col.header}>{col.render(row)}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Compile error: Order has no "id" field
// interface Order { orderNumber: string; total: number; }
// <DataTable rows={orders} columns={...} />

5. Praxisbeispiel: Eine generische Select-Komponente

Das klassische Beispiel für generische React-Komponenten ist eine wiederverwendbare Select-Komponente, die für Produkte, Nutzer, Kategorien oder beliebige andere Datentypen funktionieren soll, ohne dass für jeden Typ eine eigene Komponente entsteht. Der Typparameter T beschreibt den Typ jeder einzelnen Option, während zwei Callback-Props, getLabel und getValue, festlegen, wie aus einem konkreten T-Wert der angezeigte Text beziehungsweise der interne Wert abgeleitet wird.

Diese Struktur trennt die generische Darstellungslogik vollständig von der konkreten Datenform. Die Select-Komponente selbst muss nicht wissen, ob sie Produkte oder Nutzer anzeigt, sie verlässt sich ausschließlich auf die beiden Callback-Funktionen. Der Compiler sorgt trotzdem dafür, dass onChange exakt den Typ T zurückgibt, den die jeweilige Aufrufstelle erwartet, sodass am Ende der Auswahlkette keine manuelle Typumwandlung nötig ist.


interface SelectProps<T> {
  options: T[];
  value: T | null;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
  onChange: (option: T) => void;
}

function Select<T>({ options, value, getLabel, getValue, onChange }: SelectProps<T>) {
  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selected = options.find((opt) => getValue(opt) === e.target.value);
    if (selected) onChange(selected);
  };

  return (
    <select value={value ? getValue(value) : ""} onChange={handleChange}>
      {options.map((opt) => (
        <option key={getValue(opt)} value={getValue(opt)}>{getLabel(opt)}</option>
      ))}
    </select>
  );
}

// Usage with Product: T is inferred, onChange receives a full Product
// <Select
//   options={products}
//   value={selectedProduct}
//   getLabel={(p) => p.name}
//   getValue={(p) => String(p.id)}
//   onChange={setSelectedProduct}
// />

6. Generics mit forwardRef kombinieren

Sobald eine generische Komponente zusätzlich eine Ref an ein natives DOM-Element weiterreichen soll, etwa damit die aufrufende Komponente den Fokus programmatisch setzen kann, kommt forwardRef ins Spiel. Die Herausforderung: forwardRef selbst ist in älteren React-Typdefinitionen nicht generisch typisiert, was dazu führt, dass der Typparameter der umschlossenen Komponente beim direkten Einsatz verloren geht und implizit zu unknown wird. In der Praxis behilft man sich entweder mit einer expliziten Typ-Assertion beim Export oder nutzt die generische forwardRef-Signatur, die neuere React-Typdefinitionen mitbringen.

Der zuverlässigste Weg bleibt, die Komponente intern generisch zu implementieren und den Typparameter beim Export über eine Assertion wiederherzustellen, weil diese Technik unabhängig von der jeweiligen React-Typversion funktioniert. Wichtig ist dabei, den internen Namen und den exportierten Namen bewusst zu trennen, damit die Assertion an genau einer, klar erkennbaren Stelle steht statt verteilt über die gesamte Komponente.


import { forwardRef } from "react";
import type { Ref } from "react";

interface SearchInputProps<T> {
  suggestions: T[];
  getLabel: (item: T) => string;
  onSelect: (item: T) => void;
}

// Internal implementation stays generic
function SearchInputInner<T>(
  { suggestions, getLabel, onSelect }: SearchInputProps<T>,
  ref: Ref<HTMLInputElement>
) {
  return (
    <div>
      <input ref={ref} type="text" placeholder="Suchen..." />
      <ul>
        {suggestions.map((item, i) => (
          <li key={i} onClick={() => onSelect(item)}>{getLabel(item)}</li>
        ))}
      </ul>
    </div>
  );
}

// Restore the generic type parameter with a single, explicit assertion at export
export const SearchInput = forwardRef(SearchInputInner) as <T>(
  props: SearchInputProps<T> & { ref?: Ref<HTMLInputElement> }
) => ReturnType<typeof SearchInputInner>;

7. Default-Typparameter für seltene Sonderfälle

Manche generischen React-Komponenten werden in der überwiegenden Mehrheit der Fälle mit demselben Typ verwendet, sollen aber trotzdem für seltene Sonderfälle offen bleiben. Für diese Situation erlaubt TypeScript einen Default-Typparameter, etwa interface DropdownProps<T = string>. Wird die Komponente ohne explizites Typargument und ohne aus Props ableitbaren Typ verwendet, greift der Default-Typ, hier string, ohne dass die Aufrufstelle irgendetwas Zusätzliches angeben muss.

Dieses Muster reduziert Schreibarbeit an den häufigen Aufrufstellen erheblich, ohne die Flexibilität für die selteneren Fälle zu verlieren. Ein Dropdown, das in neun von zehn Fällen einfache Text-Optionen anzeigt, aber gelegentlich auch komplexere Objekte verarbeiten soll, profitiert genau von dieser Kombination aus Default-Typ und weiterhin voller Generizität für die Ausnahmefälle.


// Default type parameter: T falls back to string when nothing else fits
interface DropdownProps<T = string> {
  options: T[];
  onSelect: (option: T) => void;
}

function Dropdown<T = string>({ options, onSelect }: DropdownProps<T>) {
  return (
    <ul>
      {options.map((opt, i) => (
        <li key={i} onClick={() => onSelect(opt)}>{String(opt)}</li>
      ))}
    </ul>
  );
}

// Common case: no explicit type argument, defaults to string
// <Dropdown options={["EUR", "USD", "CHF"]} onSelect={setCurrency} />

// Rare case: explicit type argument overrides the default
// <Dropdown<Product> options={products} onSelect={setSelectedProduct} />

8. Mehrere Typparameter in einer Komponente

Genau wie generische Funktionen können auch generische React-Komponenten mehr als einen Typparameter besitzen. Eine typische Situation ist eine Komponente, die Optionen eines Typs anzeigt, aber einen Rückgabewert eines anderen Typs erzeugt, etwa eine Multi-Select-Komponente, die aus einer Liste von Objekten eine Liste von IDs zurückgibt. Zwei unabhängige Typparameter, etwa T für das Options-Objekt und V für den zurückgegebenen Wert, bilden diese Beziehung präzise ab.

Wichtig ist, die Beziehung zwischen den Typparametern nicht künstlich zu verkomplizieren. In den meisten Fällen reicht ein einziger Typparameter zusammen mit einem Callback, der die Ableitung übernimmt, wie im Select-Beispiel weiter oben. Zwei oder mehr Typparameter lohnen sich nur, wenn die Beziehung zwischen den Typen tatsächlich unabhängig variieren kann, etwa wenn dieselbe Options-Struktur mit unterschiedlichen Rückgabewert-Typen kombinierbar sein soll.

9. Generisch, Union oder any: der direkte Vergleich

Nicht jede Komponente mit mehreren Datentypen braucht zwingend echte Generics. Die folgende Übersicht zeigt, wann sich ein generischer Typparameter lohnt und wann eine einfachere Alternative ausreicht.

Situation Unpassender Ansatz Empfohlener Ansatz Begründung
Nur ein einziger Datentyp dauerhaft List<T> mit fest einem Aufruf ProductList ohne Generic Kein Nutzen ohne echte Wiederverwendung
Zwei bis drei bekannte Varianten Generic mit Constraint auf Union Discriminated Union in den Props Klarere Fehlermeldungen, weniger Abstraktion
Beliebig viele, unbekannte Datentypen items: any[] items: T[] mit Generic Volle Typprüfung bei jeder Verwendung
Struktur mit Mindestanforderung Generic ohne Constraint T extends { id: … } Fehler bereits beim Kompilieren statt zur Laufzeit
Häufigster Fall soll einfach bleiben Typargument immer explizit angeben Default-Typparameter T = string Kürzere Schreibweise im Regelfall

Generics sind kein Ersatz für Discriminated Unions, wenn die Anzahl der Varianten klein und bekannt ist. Sie entfalten ihren Nutzen erst, wenn eine Komponente für beliebig viele, zur Entwurfszeit nicht abschließend bekannte Datentypen funktionieren soll. Wer diesen Unterschied im Blick behält, vermeidet sowohl unnötig komplizierte generische Konstrukte für simple Fälle als auch unsichere any-Fallbacks für Fälle, die eigentlich vollständig typisierbar wären.

Mironsoft

TypeScript-Tooling, wiederverwendbare React-Komponenten und Magento/Hyvä-Integrationen

Duplizierte Komponenten zu generischen Bausteinen zusammenführen?

Wir identifizieren strukturell identische React-Komponenten in eurer Codebase und bündeln sie zu generischen, vollständig typsicheren Bausteinen mit Constraints, forwardRef-Unterstützung und Default-Typparametern.

Duplikat-Analyse

Strukturell ähnliche Komponenten in der Codebase identifizieren

Generic-Refactoring

Zusammenführung zu einer generischen, typsicheren Komponente

Komponenten-Bibliothek

Aufbau einer wiederverwendbaren, generischen UI-Bibliothek fürs Team

10. Zusammenfassung

Generische React-Komponenten lösen das Problem strukturell identischer Komponenten, die sich nur im verarbeiteten Datentyp unterscheiden. Ein Typparameter an der Funktionskomponente, kombiniert mit passenden Props für Rendering und Callbacks, ersetzt mehrere fast identische Komponenten durch eine einzige, vollständig typsichere Implementierung. Type Inference in JSX übernimmt in der Praxis die meiste Arbeit automatisch, Constraints wie T extends { id: … } sichern zusätzlich Mindestanforderungen an den Typparameter ab, ohne die Flexibilität einzuschränken.

Für Sonderfälle wie die Kombination mit forwardRef oder Default-Typparameter für den häufigsten Anwendungsfall gibt es etablierte Lösungen, die ohne Kompromisse bei der Typsicherheit auskommen. Wichtig bleibt die Abgrenzung: Bei einer kleinen, bekannten Anzahl an Varianten ist eine Discriminated Union oft die klarere Wahl, echte Generics lohnen sich erst, wenn eine Komponente für beliebig viele, nicht abschließend bekannte Datentypen funktionieren soll.

Generische React-Komponenten - Das Wichtigste auf einen Blick

Grundsyntax

Typparameter direkt nach dem Funktionsnamen, benannte Funktion statt Arrow Function wegen JSX-Kollision.

Type Inference

Der Compiler leitet T meist automatisch aus der items-Prop ab, explizites Typargument nur bei leeren Arrays nötig.

Constraints & forwardRef

T extends { id: … } für Mindestanforderungen, Assertion beim Export für generische forwardRef-Komponenten.

Wann Generics sich lohnen

Bei beliebig vielen Datentypen. Bei wenigen bekannten Varianten ist eine Discriminated Union oft klarer.

11. FAQ: Generische React-Komponenten

1Wie deklariere ich eine generische React-Komponente?
Als benannte Funktion mit Typparameter direkt nach dem Namen. Arrow Functions brauchen in .tsx ein Komma nach dem Typparameter.
2Muss der Typ immer explizit angegeben werden?
Nein, meist leitet TypeScript ihn automatisch aus einer Prop wie items ab. Explizit nötig nur ohne konkreten Anhaltspunkt, etwa bei leeren Arrays.
3Was ist ein Constraint bei Generics?
Schränkt den Typparameter auf Typen mit Mindeststruktur ein, etwa T extends { id: ... }. Fehlende Felder werden beim Kompilieren abgelehnt.
4Wie kombiniere ich Generics mit forwardRef?
Intern generisch implementieren, den Typparameter beim Export über eine explizite Assertion wiederherstellen.
5Generics oder Discriminated Union?
Bei wenigen bekannten Varianten eine Discriminated Union. Generics erst bei beliebig vielen, nicht abschließend bekannten Datentypen.
6Was ist ein Default-Typparameter?
Ein Fallback wie T = string, der greift, wenn kein Typargument angegeben und keine Inferenz möglich ist.
7Kann eine Komponente mehrere Typparameter haben?
Ja, etwa T für Optionen und V für einen unabhängigen Rückgabewert, sofern beide unabhängig variieren können.
8Warum funktioniert <T> bei Arrow Functions in .tsx nicht?
Der Parser hält <T> sonst für JSX. Ein Komma nach dem Typparameter oder eine benannte Funktion lösen die Mehrdeutigkeit auf.
9Verlangsamen Generics die Laufzeit?
Nein, Typparameter verschwinden vollständig beim Kompilieren. Zur Laufzeit gibt es keinen Unterschied zu fest typisierten Komponenten.
10Wann lohnt sich der Umstieg auf eine generische Komponente?
Sobald dieselbe Struktur für zwei Typen existiert oder ein dritter Fall absehbar ist. Bei nur einem Anwendungsfall reicht eine einfache Komponente.