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:
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.
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.
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
asChildangepasst 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.
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.
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.
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 SteuerungenBody- HauptinhaltbereichFooter- 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 LabelDescription- Unterstützender Text oder erläuternder Inhalt
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
View your project's performance over time
</CardDescription>