Composition

Die Grundlage für den Aufbau moderner UI-Komponenten.

Composition, oder Komponierbarkeit, ist die Grundlage für den Aufbau moderner UI-Komponenten. Sie ist eine der mächtigsten Techniken, um flexible, wiederverwendbare Komponenten zu erstellen, die komplexe Anforderungen erfüllen können, ohne die Klarheit der API zu opfern.

Anstatt alle Funktionen in eine einzelne Komponente mit Dutzenden von Props zu quetschen, verteilt Composition die Verantwortung auf mehrere zusammenarbeitende Komponenten.

Fernando hielt dazu einen großartigen Vortrag auf der React Universe Conf 2025, in dem er seinen Ansatz zum Wiederaufbau von Slack's Message Composer als komponierbare Komponente vorstellte.

Eine Komponente komponierbar machen

Um eine Komponente komponierbar zu machen, müssen Sie sie in kleinere, fokussiertere Komponenten aufteilen. Betrachten wir zum Beispiel diese Accordion-Komponente:

accordion.tsx
import { Accordion } from '@/components/ui/accordion';

const data = [
  {
    title: 'Accordion 1',
    content: 'Accordion 1 content',
  },
  {
    title: 'Accordion 2',
    content: 'Accordion 2 content',
  },
  {
    title: 'Accordion 3',
    content: 'Accordion 3 content',
  },
];

return (
  <Accordion data={data} />
);

Obwohl diese Accordion-Komponente einfach erscheinen mag, übernimmt sie zu viele Verantwortlichkeiten. Sie ist zuständig für das Rendern des Containers, des Triggers und des Inhalts sowie für die Verwaltung des Accordion-Zustands und der Daten.

Die Anpassung des Stylings dieser Komponente ist schwierig, weil sie eng gekoppelt ist. Wahrscheinlich sind globale CSS-Überschreibungen erforderlich. Außerdem erfordert das Hinzufügen neuer Funktionen oder das Anpassen des Verhaltens Änderungen am Quellcode der Komponente.

Um das zu lösen, können wir dies in kleinere, fokussiertere Komponenten aufteilen.

1. Root-Komponente

Zuerst konzentrieren wir uns auf den Container — die Komponente, die alles zusammenhält, also Trigger und Content. Dieser Container muss nichts über die Daten wissen, muss jedoch den geöffneten Zustand verfolgen.

Wir möchten diesen Zustand jedoch auch für Kindkomponenten zugänglich machen. Verwenden wir also die Context API, um einen Kontext für den geöffneten Zustand zu erstellen.

Schließlich erweitern wir die Standard-HTML-Attribute, um die Modifikation des div-Elements zu ermöglichen.

Wir nennen diese Komponente die "Root"-Komponente.

@/components/ui/accordion.tsx
type AccordionProps = React.ComponentProps<'div'> & {
  open: boolean;
  setOpen: (open: boolean) => void;
};

const AccordionContext = createContext<AccordionProps>({
  open: false,
  setOpen: () => {},
});

export type AccordionRootProps = React.ComponentProps<'div'> & {
  open: boolean;
  setOpen: (open: boolean) => void;
};

export const Root = ({
  children,
  open,
  setOpen,
  ...props
}: AccordionRootProps) => (
  <AccordionContext.Provider value={{ open, setOpen }}>
    <div {...props}>{children}</div>
  </AccordionContext.Provider>
);

2. Item-Komponente

Die Item-Komponente ist das Element, das den Eintrag im Accordion enthält. Sie ist einfach ein Wrapper für jeden Eintrag im Accordion.

@/components/ui/accordion.tsx
export type AccordionItemProps = React.ComponentProps<'div'>;

export const Item = (props: AccordionItemProps) => <div {...props} />;

3. Trigger-Komponente

Die Trigger-Komponente ist das Element, das das Accordion beim Aktivieren öffnet. Sie ist verantwortlich für:

  • Standardmäßig als Button zu rendern (kann mit asChild angepasst werden)
  • Klickereignisse zu verarbeiten, um das Accordion zu öffnen
  • Den Fokus zu verwalten, wenn das Accordion schließt
  • Bereitstellung geeigneter ARIA-Attribute

Fügen wir diese Komponente zu unserer Accordion-Komponente hinzu.

@/components/ui/accordion.tsx
export type AccordionTriggerProps = React.ComponentProps<'button'> & {
  asChild?: boolean;
};

export const Trigger = ({ asChild, ...props }: AccordionTriggerProps) => (
  <AccordionContext.Consumer>
    {({ open, setOpen }) => (
      <button onClick={() => setOpen(!open)} {...props} />
    )}
  </AccordionContext.Consumer>
);

4. Content-Komponente

Die Content-Komponente ist das Element, das den Accordion-Inhalt enthält. Sie ist verantwortlich für:

  • Das Rendern des Inhalts, wenn das Accordion geöffnet ist
  • Bereitstellung geeigneter ARIA-Attribute

Fügen wir diese Komponente zu unserer Accordion-Komponente hinzu.

@/components/ui/accordion.tsx
export type AccordionContentProps = React.ComponentProps<'div'> & {
  asChild?: boolean;
};

export const Content = ({ asChild, ...props }: AccordionContentProps) => (
  <AccordionContext.Consumer>
    {({ open }) => <div {...props} />}
  </AccordionContext.Consumer>
);

5. Alles zusammenfügen

Nachdem wir nun alle Komponenten haben, können wir sie in unserer ursprünglichen Datei zusammenfügen.

accordion.tsx
import * as Accordion from '@/components/ui/accordion';

const data = [
  {
    title: 'Accordion 1',
    content: 'Accordion 1 content',
  },
  {
    title: 'Accordion 2',
    content: 'Accordion 2 content',
  },
  {
    title: 'Accordion 3',
    content: 'Accordion 3 content',
  },
];

return (
  <Accordion.Root open={false} setOpen={() => {}}>
    {data.map((item) => (
      <Accordion.Item key={item.title}>
        <Accordion.Trigger>{item.title}</Accordion.Trigger>
        <Accordion.Content>{item.content}</Accordion.Content>
      </Accordion.Item>
    ))}
  </Accordion.Root>
);

Namenskonventionen

Beim Erstellen komponierbarer Komponenten sind konsistente Namenskonventionen entscheidend, um intuitive und vorhersehbare APIs zu schaffen. Sowohl shadcn/ui als auch Radix UI folgen etablierten Mustern, die zum De-facto-Standard im React-Ökosystem geworden sind.

Root-Komponenten

Die Root-Komponente dient als Hauptcontainer, der alle anderen Sub-Komponenten umschließt. Sie verwaltet typischerweise gemeinsamen Zustand und Context, indem sie einen Kontext für alle Kindkomponenten bereitstellt.

<AccordionRoot>{/* Child components */}</AccordionRoot>

Interaktive Elemente

Interaktive Komponenten, die Aktionen auslösen oder Zustände umschalten, verwenden beschreibende Namen:

  • Trigger - Das Element, das eine Aktion auslöst (Öffnen, Schließen, Umschalten)
  • Content - Das Element, das den Hauptinhalt enthält, der angezeigt/ausgeblendet wird
<CollapsibleTrigger>Click to expand</CollapsibleTrigger>
<CollapsibleContent>
  Hidden content revealed here
</CollapsibleContent>

Inhaltsstruktur

Für Komponenten mit strukturierten Inhaltsbereichen verwenden Sie semantische Namen, die ihren Zweck beschreiben:

  • Header - Oberer Abschnitt mit Titeln oder Steuerungen
  • Body - Hauptinhaltbereich
  • Footer - Unterer Abschnitt für Aktionen oder Metadaten
<DialogHeader>
  {/* Dialog title */}
</DialogHeader>
<DialogBody>
  {/* Dialog content */}
</DialogBody>
<DialogFooter>
  {/* Dialog footer */}
</DialogFooter>

Informationskomponenten

Komponenten, die Informationen oder Kontext bereitstellen, verwenden beschreibende Suffixe:

  • Title - Hauptüberschrift oder Label
  • Description - Unterstützender Text oder erläuternder Inhalt
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
  View your project's performance over time
</CardDescription>