Design
Kompozycja Reactowa czyli elastyczność ponad przewidywalność.
Początek znanej debaty nad Reactem
Znane paradygmaty programowania obiektowego - dziedziczenie, abstrakcja i polimorfizm, są wpajane młodym programistom od początków nauki. Nie ma w tym nic złego. Podstawy są ważne i należy je przyswoić. Są jednak takie technologie, w których np: dziedziczenie się nie sprawdza. Jedną z takich technologii jest nasz znany i lubiany React. Kolejnym znanym społeczności zagadnieniem, jest przekazywanie właściwości komponentów. Dlaczego nawet dokumentacja głosi kult kompozycji? Temat ten pojawia się w wielu artukułach i postach, dlatego NFU postanowiło dorzucić swoje 5 groszy, w odniesieniu do znanych bibliotek frontendowych.
Dlaczego właśnie ta cała kompozycja?
Spójrzmy najpierw na rozwiązanie, w którym funckja renderująca kompoment, przyjmuje propsy i renderuje je w odpowiednich miejscach wewnątrz komponentu.
function Card(props) {
return (
<div className="card">
<div className="card-header">
<h2>{props.title}</h2>
<p>{props.description}</p>
</div>
<div className="card-content">
<p>{props.content}</p>
</div>
<div className="card-footer">
<p>{props.footer}</p>
</div>
</div>
);
}
Będzie działać. Tylko co w przypadku kiedy trzeba to będzie rozbudować? Poprostu załadujemy więcej propsów? Czyli w zasadzie można to sprowadzić do klasyka:
Skalowalność leży. Czyli jest to całkiem do kitu? No otóż nie do końca. Można wymienić zalety takie jak przewiwalność. Cieżko będzie źle przekazać parametry, bo wiemy co jest potrzebne do prawidłowego działania komponentu.
Z pomocą przychodzi nam Reactowa kompozycja. Wzorzec kompozycji w React polega na tworzeniu bardziej złożonych struktur, poprzez składanie prostszych obiektów w hierarchiczną strukturę, a nie rozszerzaniu funkcjonalności już istniejących komponentów. Dla przykładu, spójrzmy na potencjalny kod nagłówka naszej karty:
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
Składając klocek po klocku, uzyskujemy właśnie opisywaną kompozycję. Finalny kod naszej karty może prezentować się następująco:
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<p>Card Content</p>
</CardContent>
<CardFooter>
<p>Card Footer</p>
</CardFooter>
</Card>
Każdy z tych pojedynczych komponentów, oprócz wykorzystania dla naszej karty można przy okazji wykorzystać w innych miejscach. Do innych zalet tego wzorca możemy zaliczyć:
- Elastyczność - używamy jak chcemy i kiedy chcemy, szybko i sprawnie możemy zmienić układ części komponentu.
- Skalowalność - rozbudowa komponentów nie stanowi żadnego problemu.
- Łatwa konserwacja - każdy element może być zarządzany niezależnie, co ułatwia utrzymanie kodu.
Czy istnieją wady kompozycji? Istnieje ryzyko wprowadzenia zbyt wielu zależności pomiędzy komponentami, co może skutować trudnościami przy modyfikacji. Gdy struktura klocków jest już całkiem rozbudowana to samo debugowanie może być dosyć czasochłonne. Co ciekawe, jak to zazwyczaj bywa, w różnych przypadkach sprawdzają się różne podejścia i nie można powiedzieć, że jedno podejście jest zawsze lepsze od drugiego. Dużo zależy od charakteru komponentu.
A komu to potrzebne?
Jakieś żywe przykłady gdzie z tego korzystamy? Sama idea jest wpajana developerom nawet przez DOKUMENTACJĘ. Ale czy przypadkiem przedstawiony wyżej kod wydaje się być znajomy? To nic innego jak jeden z komponentów z biblioteki SHADCN. Jest ona sztandarowym przykładem wykorzystania omawianej w poście kompozycji. Sami korzystamy w każdym projekcie, które realizujemy, z resztą nie tylko my. Repo wykęciło 51 tysięcy gwiazdek (w dniu publikacji).
// zwykłe stylowanie przy pomocy Tailwind
export default function Home() {
return (
<>
<button className="p-2 bg-orange-400">Click me</button>
</>
);
}
// stylowanie przy pomocy SHADCN
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<>
<Button variant="outline">Button</Button>
</>
);
}
Dlaczego wszyscy tak lubią tą bibliotekę? Przede wszystkim za intuicyjność i konstrukcję dostosowaną do szybkiej rozbudowy czy modyfikacji komponentów.
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
Tak skonstuowany komponent jest gotowy do przyjęcia elementów potomnych, oraz gotowy do wielokrotnego wykorzystania w różnych miejsach projektu.
Podsumowanie
Warto dobrze zrozumieć i stosować wzorzec kompozycji w swoich projektach Reactowych, ponieważ przynosi on realne korzyści zarówno w procesie tworzenia, jak i utrzymywania aplikacji po stronie frontu. Dla potwierdzenia najlepiej spojrzeć właśnie na biblioteki gigantów frontowych. Skoro oni zbudowali na tym swój potencjał to musi działać!
jeszcze jeden i starczy...
Projekty
Projekt strony internetowej - ważniejszy niż implementacja?
Design