ReactNext.jsDizajnerski sustaviArhitekturaTypeScriptUI inženjering

Arhitektura React komponenti za skaliranje: obrasci za održiv dizajnerski sustav

Adrijan Omičević··13 min čitanja
Share

# Što ćete izgraditi u ovom vodiču#

Skalabilna arhitektura React komponenti manje je stvar savršenog stabla mapa, a više stvar ponovljivih odluka koje cijeli tim može primjenjivati pod pritiskom vremena.

Ovaj vodič iznosi pragmatičnu arhitekturu za velike React i Next.js codebaseove, s fokusom na kompoziciju, složene (compound) komponente, polimorfne komponente, tematiziranje i konvencije mapa. Dobit ćete i anti-uzorke koje treba izbjegavati te plan refaktoriranja koji možete provoditi sprint po sprint.

Na kraju ćete imati strukturu koja podržava održiv dizajnerski sustav, bez pretvaranja UI sloja u kruti framework.

# Zašto se arhitektura React komponenti raspada na velikom opsegu#

Većina timova počne s dobrim namjerama i završi s UI entropijom: više implementacija gumba, nekonzistentni razmaci i komponente koje rade samo na jednom ekranu.

Trošak se brzo vidi:

  • Sporija isporuka: inženjeri ponovno implementiraju UI umjesto da ga ponovno koriste ili troše vrijeme na dešifriranje “koji je Button onaj pravi”.
  • Umnožavanje bugova: popravak problema pristupačnosti u tri verzije iste komponente su tri prilike da jednu propustite.
  • Regresije performansi: previše konfigurabilne komponente i kaskade ponovnih rendera potaknute propsima česte su u velikim UI sustavima.

ℹ️ Napomena: Dizajnerski sustavi najčešće propadaju zbog governancea i arhitekture, a ne zbog boja i tipografije. Ako se vaše komponente teško komponiraju, timovi će ih zaobilaziti.

Ako gradite na Next.js, arhitektonske odluke utječu i na server rendering te granice dohvaćanja podataka. Kad miješate interaktivnost i layout bez jasnih granica, veća je vjerojatnost da ćete stvoriti nepotrebne client bundleove i dodatni hydration posao. Za dublji mentalni model pogledajte naš vodič o React Server Components u Next.js.

# Temeljna načela: pravila koja sustav čine održivim#

Uzorci u nastavku mogu se implementirati na više načina, ali sustav drži samo ako je nekoliko pravila dosljedno.

1) Dajte prednost kompoziciji u odnosu na konfiguraciju#

Ako komponenta ima više od 10 propsa i pola njih se rijetko koristi, to je obično znak da pokušavate jednom komponentom pokriti previše layouta.

Kompozicija čuva API-e malima, potiče ponovno korištenje i sprječava eksploziju varijanti.

2) “Komponente dizajnerskog sustava” neka po defaultu budu prezentacijske#

Interaktivne komponente teže je održati stabilnima jer se tokovi stanja razlikuju od featurea do featurea.

Krenite od prezentacijskih komponenti i izložite hookove ili manje interaktivne wrapere kad je potrebno.

3) Učinite “sretni put” najlakšim putem#

Ako korištenje dizajnerskog sustava zahtijeva dodatne wrapere, nezgodne propse ili čitanje internih dokumenata, inženjeri će copy-pasteati.

Ergonomija je governance.

4) Eksplicitno definirajte granice#

Trebate granice za:

  • stiliziranje i tematiziranje
  • klijentske naspram serverskih komponenti u Next.js
  • domenski UI naspram UI-a dizajnerskog sustava

Granica je mapa, pravilo izvoza i pravilo ovisnosti.

# Skalabilna konvencija mapa i izvoza#

Česta greška je jedna components/ mapa sa stotinama datoteka. Postane ladica za svašta.

Evo strukture koja se skalira, a ostaje praktična u Next.js:

PodručjeMapaŠto ide ovdjePravilo ovisnosti
Primitive dizajnerskog sustavasrc/ui/primitives/Button, Input, Text, Stack, IconBez app importova
Složene komponente dizajnerskog sustavasrc/ui/components/Modal, Dropdown, DatePicker, DataTableMože koristiti primitive
UI featureasrc/features/*/components/Komponente specifične za ekranMože koristiti src/ui/*
Logika featureasrc/features/*/hooks/Hookovi za stanje featurea, podatkeNisu potrebni importovi iz src/ui
App rutesrc/app/Next.js rute i layoutiKoristi features i ui
Dijeljeni utilityjisrc/lib/fetcheri, formateri, loggingIdealno bez React importova

Strategija izvoza jednako je važna kao i mape.

  • src/ui/index.ts izvozi stabilne javne UI API-e.
  • Izbjegavajte duboke importe poput src/ui/primitives/button/Button.tsx jer stvaraju tijesnu vezu i čine refaktore bolnima.
  • Interna pomagala držite neizvezenima ili u internal/ mapama.

💡 Savjet: Granice provedite ESLint pravilima. Jednostavno pravilo “bez cross-feature importova” sprječava da features/billing importira features/auth komponente i tiho stvara cikluse.

# Obrazci kompozicije koji se skaliraju#

Obrazac 1: Slotovi umjesto boolean propsa#

Umjesto hasIcon, showSubtitle, withBadge, koristite slotove.

Loše dizajnirani API-i često vode do uvjetnog špageta koda unutar komponente i desetaka “gotovo istih” varijanti.

TSX
```typescript
type CardProps = \\{
  header?: React.ReactNode;
  footer?: React.ReactNode;
  children: React.ReactNode;
\\};
 
export function Card(props: CardProps) \\{
  return (
    <div className="rounded-xl border p-4">
      \\{props.header ? <div className="mb-3">\\{props.header\\}</div> : null\\}
      <div>\\{props.children\\}</div>
      \\{props.footer ? <div className="mt-3">\\{props.footer\\}</div> : null\\}
    </div>
  );
\\}
Plaintext
 
Ovaj se API skalira jer se nove potrebe implementiraju kao komponirani UI, a ne kao dodatni propsi.
 
### Obrazac 2: “Layout primitive” za smanjenje ad-hoc CSS-a
 
Ako svaki ekran definira vlastite razmake, vaš UI će se s vremenom razići. Layout primitive čine razmake konzistentnima i ubrzavaju razvoj.
 
Uobičajene primitive uključuju Stack, Inline, Grid, Container. Trebale bi biti dosadne i predvidljive.
 
Ako koristite utility CSS, držite ga discipliniranim. Ovo se dobro slaže s [najboljim praksama za Tailwind CSS](https://samioda.com/en/blog/tailwind-css-best-practices), posebno oko konzistentnih skala razmaka i izbjegavanja proizvoljnih vrijednosti.
 
### Obrazac 3: Kontrolirane i nekontrolirane komponente s jednim mentalnim modelom
 
Za inpute, dropdownove, tabove i modale podržite oba obrasca:
 
- kontrolirano: parent posjeduje state
- nekontrolirano: komponenta posjeduje state, parent sluša
 
Učinite API eksplicitnim s `value` i `defaultValue`, `open` i `defaultOpen`.
 
| Komponenta | Kontrolirani propsi | Nekontrolirani propsi | Obavezni callbackovi |
| --- | --- | --- | --- |
| Tabs | `value` | `defaultValue` | `onValueChange` |
| Modal | `open` | `defaultOpen` | `onOpenChange` |
| Input | `value` | `defaultValue` | `onChange` |
 
Ovo smanjuje potrebu za custom wrapperima i čini komponente upotrebljivima i na jednostavnim i na složenim ekranima.
 
## Složene (compound) komponente: pragmatičan pristup
 
Složene komponente daju čist API za kompleksne UI strukture bez guranja goleme površine propsa.
 
Ideja je: jedan parent koordinira zajedničko stanje i context, a child komponente ga koriste.
 
### Primjer: Tabs kao složene (compound) komponente
 
```tsx
```typescript
import * as React from "react";
 
type TabsContextValue = \\{
  value: string;
  setValue: (v: string) => void;
\\};
 
const TabsContext = React.createContext<TabsContextValue | null>(null);
 
export function Tabs(props: \\{ defaultValue: string; children: React.ReactNode \\}) \\{
  const [value, setValue] = React.useState(props.defaultValue);
  return <TabsContext.Provider value=\\{\\{ value, setValue \\}\\}>\\{props.children\\}</TabsContext.Provider>;
\\}
 
export function TabsList(props: \\{ children: React.ReactNode \\}) \\{
  return <div className="flex gap-2">\\{props.children\\}</div>;
\\}
 
export function Tab(props: \\{ value: string; children: React.ReactNode \\}) \\{
  const ctx = React.useContext(TabsContext);
  if (!ctx) throw new Error("Tab must be used within Tabs");
  const active = ctx.value === props.value;
 
  return (
    <button
      type="button"
      className=\\{active ? "font-semibold" : "opacity-70"\\}
      onClick=\\{() => ctx.setValue(props.value)\\}
    >
      \\{props.children\\}
    </button>
  );
\\}
 
export function TabsPanel(props: \\{ value: string; children: React.ReactNode \\}) \\{
  const ctx = React.useContext(TabsContext);
  if (!ctx) throw new Error("TabsPanel must be used within Tabs");
  if (ctx.value !== props.value) return null;
  return <div className="pt-4">\\{props.children\\}</div>;
\\}
Plaintext
 
Korištenje ostaje čitljivo i prilagodljivo:
 
- dodajte ikone u sadržaj `Tab` bez novih propsa
- dodajte badgeve, keyboard handling ili analitiku bez mijenjanja svakog mjesta korištenja
 
### Kada se složene (compound) komponente isplate
 
Koristite ih kada imate:
 
- zajedničko stanje preko više poddijelova
- potrebu za fleksibilnim layoutom i prilagođenim sadržajem
- ponavljajuće obrasce “komponenta unutar komponente”
 
Nemojte ih koristiti kada je dovoljno jednostavno prop-driven rješenje. Pretjerano korištenje contexta dodaje indirekciju i otežava debugiranje.
 
> **⚠️ Upozorenje:** Česta zamka je izvoz internog contexta ili oslanjanje na context u leaf komponentama daleko od parenta. Držite compound grupacije tijesnima i ko-lociranima kako biste izbjegli implicitne ovisnosti.
 
## Polimorfne komponente bez kaosa u API-u
 
Polimorfne komponente omogućuju potrošačima da odaberu koji se element renderira, tipično preko `as` propa. To smanjuje duplikacije poput ButtonLink, ButtonAnchor, ButtonRouterLink.
 
Dobar polimorfni pristup mora zadržati type safety i ne smije “curiti” implementacijske detalje.
 
### Primjer: tipizirani polimorfni Button
 
```tsx
```typescript
import * as React from "react";
 
type PropsOf<E extends React.ElementType> = React.ComponentPropsWithoutRef<E>;
 
type ButtonProps<E extends React.ElementType> = \\{
  as?: E;
  variant?: "solid" | "outline";
\\} & Omit<PropsOf<E>, "as" | "color">;
 
export function Button<E extends React.ElementType = "button">(
  props: ButtonProps<E>
) \\{
  const \\{ as, variant = "solid", className, ...rest \\} = props;
  const Comp = as ?? "button";
  const base = "inline-flex items-center justify-center rounded-md px-3 py-2";
  const styles = variant === "solid" ? "bg-black text-white" : "border";
  return <Comp className=\\{[base, styles, className].filter(Boolean).join(" ")\\} \\{...rest\\} />;
\\}
Plaintext
 
Primjeri korištenja:
 
- `<Button onClick={...}>Save</Button>`
- `<Button as="a" href="/pricing">Pricing</Button>`
 
### Ograde (guardrails) za polimorfizam
 
Polimorfizam je moćan, ali se lako zloupotrebljava. Držite ga uskim:
 
- Koristite ga za primitive poput Button, Text, Box.
- Izbjegavajte `as` na kompleksnim komponentama poput DataTable ili Modal.
- U dokumentaciji i testovima osigurajte obavezne accessibility propse po elementu.
 
## Theming: prvo tokeni, tek onda stilovi
 
Theming je često razlika između “biblioteke komponenti” i pravog dizajnerskog sustava.
 
Skalabilan pristup:
 
1) definirajte design tokene
2) mapirajte tokene na CSS varijable
3) komponente neka koriste tokene, ne sirove boje
 
### Model tokena koji se skalira
 
| Vrsta tokena | Primjer tokena | Zašto je važno |
| --- | --- | --- |
| Semantičke boje | `--color-bg-surface` | Omogućuje light i dark temu bez prepisivanja komponenti |
| Tipografija | `--font-size-sm` | Održava tekst konzistentnim kroz ekrane |
| Razmaci | `--space-4` | Sprječava drift proizvoljnih razmaka |
| Radijus | `--radius-md` | Usklađuje zaobljenja kroz sustav |
| Sjena | `--shadow-sm` | Konzistentno kontrolira elevaciju |
 
### Minimalna postavka CSS varijabli
 
```css
```css
:root \\{
  --color-bg-surface: #ffffff;
  --color-fg: #111111;
  --space-4: 16px;
  --radius-md: 10px;
\\}
 
[data-theme="dark"] \\{
  --color-bg-surface: #0b0b0b;
  --color-fg: #f5f5f5;
\\}
Plaintext
 
Zatim komponente referenciraju tokene:
 
- background koristi `--color-bg-surface`
- tekst koristi `--color-fg`
- padding koristi `--space-4`
 
Ovo radi i s Tailwindom, ali disciplina se mora provoditi. Ako vaš UI koristi jednokratne hex vrijednosti ili proizvoljne razmake, theming će uvijek biti djelomičan.
 
Radi performansi, promjene teme držite jeftinima korištenjem CSS varijabli umjesto rerenderiranja stabala komponenti. Ovo se također uklapa u šire prakse [optimizacije performansi web stranice](https://samioda.com/en/blog/website-performance-optimization), jer smanjuje layout thrash i JS-driven ažuriranja stilova.
 
## Anti-uzorci koji ubijaju održivost
 
Anti-uzorci su vrijedni jer pokazuju što treba odmah prestati raditi.
 
### Anti-uzorak 1: “God komponente” s desecima propsa
 
Simptomi:
 
- više boolean vrijednosti koje međusobno čudno djeluju
- `variant` prop s 10 opcija
- mnogo propsa koji se izravno ulijevaju u logiku class nameova
 
Smjer refaktoriranja:
 
- razdvojite na primitive i složene wrapere
- layout odluke premjestite u kompozicijske slotove
 
### Anti-uzorak 2: Stiliziranje copy-pasteanjem stringova klasa
 
Ako se isti popis klasa pojavljuje u 10 komponenti, vaš dizajnerski sustav već postoji, ali je implicitan i bez governancea.
 
Smjer refaktoriranja:
 
- izdvojite primitive
- uvedite tokene za razmake i boje
- standardizirajte jedan izvor istine za zajedničke stilove
 
### Anti-uzorak 3: Miješanje dohvaćanja podataka i renderiranja UI-a unutar reusable komponenti
 
Reusable komponenta koja zove `fetch` ili čita iz feature storea postaje tijesno vezana uz jednu domenu.
 
Smjer refaktoriranja:
 
- komponente dizajnerskog sustava prihvaćaju podatke preko propsa
- feature sloj posjeduje dohvaćanje podataka
- u Next.js App Routeru, gurajte dohvaćanje podataka prema server komponentama i prosljeđujte propsove prema dolje
 
### Anti-uzorak 4: “Wrapper komponente” koje samo preimenuju propsove
 
Wrapper koji samo mapira `primary` na `variant="solid"` dodaje indirekciju i umnaža datoteke.
 
Smjer refaktoriranja:
 
- poboljšajte API bazne komponente
- kreirajte složene komponente samo kad dodaju ponašanje ili strukturu, ne preimenovanje
 
> **🎯 Ključna poruka:** Ako ne možete u jednoj rečenici objasniti zašto komponenta postoji, vjerojatno ne pripada dizajnerskom sustavu.
 
## Plan timskog refaktoriranja koji možete provoditi kroz sprintove
 
Većina timova ne može pauzirati razvoj featurea radi potpunog rewrita. Cilj je postupno poboljšavati arhitekturu dok isporučujete.
 
### Korak 1: Napravite inventuru i izmjerite duplikacije
 
Krenite s činjenicama, ne mišljenjima:
 
- popišite nazive komponenti koje postoje više puta: Button, Modal, Card, Input
- prebrojite koliko call siteova ima svaka varijanta
- identificirajte UI bugove koji se ponavljaju kroz ekrane, posebno pristupačnost i razmake
 
Jednostavna heuristika: ako imate 3+ implementacije istog UI obrasca, konsolidirajte.
 
### Korak 2: Definirajte “javni UI” i zamrznite ga
 
Odaberite jednu izvoznu površinu za korištenje UI-a, npr. `src/ui/index.ts`, i obvežite se da će se u novom kodu koristiti isključivo to.
 
Zatim interna mjesta tretirajte kao refaktorabilna.
 
### Korak 3: Prvo primitive, zatim kompozicija
 
Izgradite ili konsolidirajte:
 
- Button
- Text
- Input
- Stack i Inline
- Icon
 
Tek nakon što su primitive stabilne, gradite složene komponente poput Dropdown, Modal, Toast.
 
Tako izbjegavate gradnju kompleksnih komponenti na nestabilnim temeljima.
 
### Korak 4: Migrirajte inkrementalno uz codemodove i lint pravila
 
Možete migrirati ekran po ekran. Progres osigurajte tako da spriječite nova korištenja legacy komponenti.
 
Praktične ideje za provedbu:
 
- eslint pravilo: zabrani importe iz `src/components/legacy`
- eslint pravilo: prisili `src/ui` import putanju
- CI provjera: fail ako se dodaju nove datoteke u `legacy/`
 
### Korak 5: Uvedite theming bez prepisivanja svega
 
Dodajte CSS varijable i postupno mapirajte stare boje:
 
- krenite s tokenima za background i tekst
- zatim rubovi, sjene i focus ringovi
- na kraju semantički tokeni poput success, warning, danger
 
Pristup “sve odjednom” je ono što ubija momentum.
 
### Korak 6: Dodajte dokumentaciju koja odgovara na jedno pitanje po komponenti
 
Dokumentacija treba biti kratka i praktična:
 
- koji problem rješava
- tipično korištenje
- zahtjevi za pristupačnost
- koji propsi su stabilni, a koji napredni
 
Ako još nemate docs site, krenite s `README.md` po mapi komponente. Bolje je od plemenskog znanja.
 
### Korak 7: Dodajte testove tamo gdje su kvarovi skupi
 
Ne treba vam 100 posto pokrivenosti. Fokusirajte se na komponente koje mogu slomiti mnogo ekrana:
 
- Button: disabled stanje, focus stilovi
- Modal: ponašanje tipkovnice i focus trap
- Form komponente: labele, greške, aria atributi
 
UI testove držite malima i stabilnima. Snapshot testiranje velikih stabala obično je šumovito.
 
## Next.js-specifične arhitektonske napomene za dizajnerske sustave
 
### Većinu UI-a držite kompatibilnom sa serverom
 
U Next.js App Routeru komponenta postaje client komponenta ako koristi hookove ili event handlere. Ako dizajnerski sustav učini sve client-only, isporučit ćete više JS-a nego što je potrebno.
 
Praktično pravilo:
 
- primitive poput Text, Box, Card trebaju biti server-kompatibilne
- interaktivne komponente poput Dropdown, Dialog, Tabs mogu biti client komponente
- izolirajte interaktivnost u leaf komponentama
 
Ovo mjerljivo utječe na veličinu bundlea i cijenu hydracije, posebno na stranicama bogatim sadržajem.
 
### Izbjegavajte implicitne client granice
 
Ako je parent komponenta client-only, sva djeca postaju client-rendered. U dizajnerskim sustavima to može slučajno povući velika UI stabla u client bundle.
 
Interaktivne wrapere držite malima i dopustite da ih server-rendered layouti kompozicijski koriste.
 
## Ključne poruke
 
- Koristite kompoziciju i slotove kako bi API-i komponenti ostali mali i kako biste spriječili eksploziju varijanti.
- Primijenite složene (compound) komponente za kompleksne UI obrasce koji dijele stanje kroz poddijelove, i držite granicu contexta tijesnom.
- Koristite polimorfne komponente samo za primitive, uz stroge ograde kako biste izbjegli tipovni i accessibility kaos.
- Implementirajte theming pomoću tokena i CSS varijabli tako da se tema mijenja bez rerenderiranja stabala komponenti.
- Provodite granice mapa i javne izvoze lint pravilima kako biste zaustavili cross-feature coupling i širenje legacy koda.
- Refaktorirajte inkrementalno uz inventuru, konsolidaciju “prvo primitive”, migracijska pravila i ciljane testove.
 
## Zaključak
 
Skalabilna arhitektura React komponenti je skup ograničenja kojih se vaš tim dogovorno drži, a ne jednokratni refaktor. Krenite definiranjem granica, konsolidirajte primitive i standardizirajte obrasce kompozicije kako bi nove funkcionalnosti automatski bile usklađene s dizajnerskim sustavom.
 
Ako želite pomoć u reviziji vaše trenutne biblioteke komponenti, definiranju plana migracije ili implementaciji token-based teme u Next.js codebaseu, Samioda vam može pomoći da isporučite održiv dizajnerski sustav bez pauziranja isporuke. Javite se i predložit ćemo praktičan plan refaktoriranja prilagođen vašem repozitoriju i veličini tima.

FAQ

Share
A
Adrijan OmičevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.