Komposition

Grunden för att bygga moderna UI-komponenter.

Komposition, eller komponerbarhet, är grunden för att bygga moderna UI-komponenter. Det är en av de mest kraftfulla teknikerna för att skapa flexibla, återanvändbara komponenter som kan hantera komplexa krav utan att offra API-klarspråk.

Istället för att trycka in all funktionalitet i en enda komponent med dussintals props, fördelar komposition ansvaret över flera samverkande komponenter.

Fernando höll ett utmärkt föredrag om detta på React Universe Conf 2025, där han delade med sig av sin metod för att återskapa Slack's Message Composer som en komponerbar komponent.

Göra en komponent komponerbar

För att göra en komponent komponerbar behöver du dela upp den i mindre, mer fokuserade komponenter. Till exempel, låt oss ta denna Accordion-komponent:

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} />
);

Även om denna Accordion-komponent kan verka enkel, hanterar den för många ansvarsområden. Den ansvarar för att rendera containern, triggern och innehållet; samt för att hantera accordions tillstånd och data.

Att anpassa stilningen av denna komponent är svårt eftersom den är tätt kopplad. Den kräver sannolikt globala CSS-överskrivningar. Dessutom kräver det att lägga till ny funktionalitet eller justera beteendet att man modifierar komponentens källkod.

För att lösa detta kan vi bryta ned detta i mindre, mer fokuserade komponenter.

1. Root Component

Först fokuserar vi på containern - komponenten som håller allt tillsammans, dvs. triggern och innehållet. Denna container behöver inte känna till datan, men den behöver hålla koll på öppet/stängt-tillståndet.

Vi vill dock också göra detta tillstånd tillgängligt för barnkomponenter. Så låt oss använda Context API för att skapa en kontext för öppet-tillståndet.

Slutligen, för att tillåta modifiering av div-elementet, kommer vi att utöka standard HTML-attributen.

Vi kommer att kalla denna komponent "Root"-komponenten.

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

Item-komponenten är elementet som innehåller ett accordion-item. Det är helt enkelt en wrapper för varje item i accordien.

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

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

3. Trigger Component

Trigger-komponenten är elementet som öppnar accordien när det aktiveras. Den ansvarar för:

  • Rendera som en knapp som standard (kan anpassas med asChild)
  • Hantera klickhändelser för att öppna accordien
  • Hantera fokus när accordien stängs
  • Tillhandahålla korrekta ARIA-attribut

Låt oss lägga till denna komponent i vår Accordion-komponent.

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

Content-komponenten är elementet som innehåller accordion-innehållet. Den ansvarar för:

  • Rendera innehållet när accordien är öppen
  • Tillhandahålla korrekta ARIA-attribut

Låt oss lägga till denna komponent i vår Accordion-komponent.

@/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. Sätta ihop allt

Nu när vi har alla komponenter kan vi sätta ihop dem i vår ursprungliga fil.

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>
);

Namngivningskonventioner

När du bygger komponerbara komponenter är konsekventa namngivningskonventioner avgörande för att skapa intuitiva och förutsägbara API:er. Både shadcn/ui och Radix UI följer etablerade mönster som har blivit de facto-standard i React-ekosystemet.

Root Components

Root-komponenten fungerar som huvudcontainern som omsluter alla andra subkomponenter. Den hanterar vanligtvis delat tillstånd och kontext genom att tillhandahålla en kontext till alla barnkomponenter.

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

Interactive Elements

Interaktiva komponenter som triggar åtgärder eller växlar tillstånd använder beskrivande namn:

  • Trigger - Elementet som initierar en åtgärd (öppning, stängning, växling)
  • Content - Elementet som innehåller huvudinnehållet som visas/döljas
<CollapsibleTrigger>Click to expand</CollapsibleTrigger>
<CollapsibleContent>
  Hidden content revealed here
</CollapsibleContent>

Content Structure

För komponenter med strukturerade innehållsområden, använd semantiska namn som beskriver deras syfte:

  • Header - Toppsektion som innehåller titlar eller kontroller
  • Body - Huvudinnehållsområde
  • Footer - Nedersta sektion för åtgärder eller metadata
<DialogHeader>
  {/* Dialog title */}
</DialogHeader>
<DialogBody>
  {/* Dialog content */}
</DialogBody>
<DialogFooter>
  {/* Dialog footer */}
</DialogFooter>

Informational Components

Komponenter som tillhandahåller information eller kontext använder beskrivande suffix:

  • Title - Primär rubrik eller etikett
  • Description - Stödtext eller förklarande innehåll
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
  View your project's performance over time
</CardDescription>