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:
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.
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.
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.
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.
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.
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 ohjaimetBody- PääsisältöalueFooter- 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 etikettiDescription- Tukiteksti tai selittävä sisältö
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
View your project's performance over time
</CardDescription>