Koostettavuus

Modernien käyttöliittymäkomponenttien rakentamisen perusta.

Koostettavuus, tai composability, on modernien käyttöliittymäkomponenttien rakentamisen perusta. Se on yksi tehokkaimmista tekniikoista joustavien, uudelleenkäytettävien komponenttien luomiseksi, jotka pystyvät käsittelemään monimutkaisia vaatimuksia ilman, että API:n selkeys kärsii.

Sen sijaan, että kaikki toiminnallisuus ahtauttaisiin yhteen komponenttiin kymmenien propien kanssa, koostettavuus jakaa vastuun useiden yhteistyössä toimivien komponenttien kesken.

Fernando piti tästä erinomaisen esityksen React Universe Conf 2025 -tapahtumassa, jossa hän jakoi lähestymistapansa Slackin Message Composerin rakentamiseen uudelleen koostettavana komponenttina.

Komponentin tekeminen koostettavaksi

Jotta komponentista tulisi koostettava, se täytyy pilkkoa pienempiin, tarkemmin fokusoituihin komponentteihin. Otetaan esimerkiksi tämä Accordion-komponentti:

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

Vaikka tämä Accordion-komponentti saattaa vaikuttaa yksinkertaiselta, se käsittelee liian monia vastuita. Se on vastuussa säiliön, triggerin ja sisällön renderöinnistä; sekä accordion-tilan ja datan käsittelystä.

Tämän komponentin tyylien mukauttaminen on vaikeaa, koska se on tiukasti kytketty. Se todennäköisesti vaatii globaaleja CSS-overrideja. Lisäksi uuden toiminnallisuuden lisääminen tai käyttäytymisen hienosäätö vaatii komponentin lähdekoodin muokkaamista.

Ratkaisuna voimme jakaa tämän pienempiin, tarkemmin fokusoituihin komponentteihin.

1. Root-komponentti

Aloitetaan säiliöstä — komponentista, joka pitää kaiken koossa, eli triggeristä ja sisällöstä. Tämän säiliön ei tarvitse tietää datasta, mutta sen täytyy pitää kirjaa avoimesta tilasta.

Haluamme kuitenkin, että tämä tila on lapsikomponenttien saatavilla. Käytetään siis Context API:a luodaksemme kontekstin avoimelle tilalle.

Lopuksi, jotta div-elementtiä voidaan muokata, laajennamme oletusarvoisia HTML-attribuutteja.

Kutsumme tätä komponenttia "Root"-komponentiksi.

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

Item-komponentti on elementti, joka sisältää yksittäisen accordion-kohteen. Se on yksinkertaisesti wrapper kullekin kohteelle accordionissa.

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

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

3. Trigger-komponentti

Trigger-komponentti on elementti, joka avaa accordionin, kun sitä aktivoidaan. Se on vastuussa:

  • Renderöinnistä oletuksena napiksi (voidaan mukauttaa asChild-propin avulla)
  • Klikkitapahtumien käsittelystä accordionin avaamiseksi
  • Fokuksen hallinnasta, kun accordion sulkeutuu
  • Oikeiden ARIA-attribuuttien tarjoamisesta

Lisätään tämä komponentti Accordion-komponenttiimme.

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

Content-komponentti on elementti, joka sisältää accordionin sisällön. Se on vastuussa:

  • Sisällön renderöimisestä, kun accordion on avoinna
  • Oikeiden ARIA-attribuuttien tarjoamisesta

Lisätään tämä komponentti Accordion-komponenttiimme.

@/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. Yhdistäminen

Nyt kun meillä on kaikki komponentit, voimme yhdistää ne alkuperäiseen tiedostoomme.

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

Nimeämiskäytännöt

Kun rakennat koostettavia komponentteja, yhdenmukaiset nimeämiskäytännöt ovat ratkaisevia intuitiivisten ja ennustettavien API:en luomiseksi. Sekä shadcn/ui että Radix UI noudattavat vakiintuneita malleja, jotka ovat muodostuneet de facto -standardiksi React-ekosysteemissä.

Root-komponentit

Root-komponentti toimii pääsäiliönä, joka käärii kaikki muut alikomponentit. Se hallinnoi tyypillisesti jaettua tilaa ja kontekstia tarjoamalla kontekstin kaikille lapsikomponenteille.

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

Interaktiiviset elementit

Interaktiiviset komponentit, jotka laukaisevat toimintoja tai vaihtavat tiloja, käyttävät kuvaavia nimiä:

  • Trigger - Elementti, joka aloittaa toiminnon (avaaminen, sulkeminen, kytkeminen)
  • Content - Elementti, joka sisältää pääasiallisen sisällön, joka näytetään/piilotetaan
<CollapsibleTrigger>Click to expand</CollapsibleTrigger>
<CollapsibleContent>
  Hidden content revealed here
</CollapsibleContent>

Sisällön rakenne

Komponenteille, joilla on jäsenneltyjä sisältöalueita, käytä semanttisia nimiä, jotka kuvaavat niiden tarkoitusta:

  • Header - Yläsivu, joka sisältää otsikot tai ohjaimet
  • Body - Pääsisältöalue
  • Footer - Alatunniste alue toimille tai metadataan
<DialogHeader>
  {/* Dialog title */}
</DialogHeader>
<DialogBody>
  {/* Dialog content */}
</DialogBody>
<DialogFooter>
  {/* Dialog footer */}
</DialogFooter>

Informatiiviset komponentit

Komponentit, jotka tarjoavat tietoa tai kontekstia, käyttävät kuvaavia päätteitä:

  • Title - Ensisijainen otsikko tai etiketti
  • Description - Tukiteksti tai selittävä sisältö
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
  View your project's performance over time
</CardDescription>