# Što ćete naučiti#
Virtualizacija React tablica je razlika između grida koji djeluje trenutačno i onoga koji gubi frameove, zakucava CPU i pretvara interakcije korisnika u traljavo, trzavo iskustvo.
U ovom vodiču izgradit ćete produkcijski spreman obrazac s TanStack Table uz TanStack Virtual i React Query: virtualizirano renderiranje, beskonačno skrolanje potkrijepljeno paginacijom na serveru, sortiranje i filtriranje na serveru, stanje sinkronizirano s URL-om, stanje selekcije koje preživljava refetcheve te optimistična ažuriranja za izmjene i grupne akcije.
Također ćete vidjeti gdje timovi često nazaduju s performansama i kako to spriječiti profiliranjem i ispravnim obrascima memoizacije. Za dublju dijagnostiku performansi u Reactu pogledajte profiliranje performansi u Reactu, memoizacija i obrasci renderiranja.
# Zašto je virtualizacija važna za data gridove#
Renderiranje velikih skupova podataka nije “samo problem dohvaćanja”. Primarno je to problem DOM-a.
Čak i moderni preglednici imaju poteškoća kada renderirate tisuće row nodova s više ćelija, ikona, inputa i uvjetnog formatiranja. Grid s 10.000 redaka i 12 stupaca lako proizvede 120.000 ćelija. Ako svaka ćelija u prosjeku ima samo 2 DOM noda, to je oko 240.000 nodova, prije nego dodate wrappere i interaktivne komponente.
Virtualizacija to rješava tako da renderira samo ono što je vidljivo plus mali overscan buffer. U praksi to znači stabilnu veličinu DOM-a poput 40 do 120 redaka, čak i ako dataset ima 200.000 redaka.
Ciljevi performansi koje biste trebali gađati#
| Metrija | Dobar cilj | Zašto je bitno |
|---|---|---|
| Renderirani redci u DOM-u | 40 do 120 | Održava layout i paint jeftinima |
| Budžet framea pri skrolanju | 16,7 ms po frameu | Potrebno za 60 fps skrolanje |
| Veličina stranice na serveru | 50 do 200 redaka | Održava payload razumnim i smanjuje round tripove |
| Debounce za filtre | 200 do 400 ms | Sprječava “oluju” requestova tijekom tipkanja |
| Stabilan ID retka | Obavezno | Omogućuje postojanost selekcije i ispravna ažuriranja cachea |
🎯 Ključna poruka: Virtualizacija smanjuje DOM posao, ne mrežni posao. Uparite je s paginacijom na serveru i cachingom kako biste riješili obje polovice problema.
# Pregled arhitekture: TanStack Table, Virtual i React Query#
Brz grid tipično ima tri sloja:
- 1Stanje tablice i logika stupaca uz TanStack Table.
- 2Renderiranje samo vidljivih redaka uz TanStack Virtual.
- 3Dohvaćanje podataka i cache uz React Query, koristeći infinite queryje za beskonačno skrolanje i mutacije za izmjene.
Preporučeni stack#
| Briga | Biblioteka | Napomena |
|---|---|---|
| Logika tablice | TanStack Table | Headless, podržava sortiranje/filtriranje na serveru |
| Virtualizacija | TanStack Virtual | Radi s varijabilnim visinama redaka, overscan |
| Dohvaćanje podataka | React Query | Caching, paginacija, optimistična ažuriranja |
| URL sinkronizacija | Next.js router ili vaš router | Tretirajte URL kao izvor istine |
Ako vaša aplikacija koristi Next.js App Router, strategija sinkronizacije URL-a treba se uklopiti u način na koji već rukujete server komponentama i client komponentama. Ako birate između data biblioteka, pročitajte React Query vs SWR u Next.js App Routeru i odaberite jedan dosljedan pristup.
# Preduvjeti#
| Zahtjev | Verzija | Napomena |
|---|---|---|
| React | 18+ | Ponašanje concurrent renderiranja je važno |
| TanStack Table | v8+ | Ovdje korišteni API-ji prate v8 obrasce |
| TanStack Virtual | v3+ | Virtualizer hook-based API |
| React Query | v5+ | Infinite queryji i mutation API-ji |
| TypeScript | Preporučeno | Pomaže da ID-jevi redaka i modeli budu ispravni |
# Korak 1: Modelirajte server API za paginaciju, sortiranje i filtriranje#
Beskonačno skrolanje je UI ponašanje. Server i dalje treba paginirati.
Kad god je moguće koristite cursor paginaciju, jer se bolje ponaša na velikim skalama i izbjegava trošak dubokih offseta. Za admin dashboarde ili manje datasete offset paginacija može i dalje biti prihvatljiva, ali cursor paginacija je sigurniji default kada broj redaka može narasti na stotine tisuća.
Predloženi oblik API-ja#
| Polje | Tip | Primjer | Napomena |
|---|---|---|---|
items | array | [{ id, ... }] | Mora sadržavati stabilan id |
nextCursor | string ili null | "eyJpZCI6..." | Null znači da nema više stranica |
total | number | 153204 | Opcionalno, skupo za velike datasete |
meta | object | { tookMs: 42 } | Korisno za debugiranje |
Parametri za sortiranje i filtriranje#
Neka budu eksplicitni i serijalizabilni kako biste ih mogli sinkronizirati s URL-om.
| Param | Primjer | Napomena |
|---|---|---|
sort | createdAt:desc | Jedno sortiranje je najjednostavnije |
filters[status] | active | Koristite stabilne ključeve |
q | acme | Globalna pretraga |
cursor | ... | Cursor za beskonačno skrolanje |
limit | 100 | Veličina stranice |
⚠️ Upozorenje: Nemojte slati “blobove stanja tablice” na server. Uvijek šaljite eksplicitne ključeve sortiranja i vrijednosti filtera. To drži API stabilnim i sprječava da refaktori na klijentu razbiju backend logiku.
# Korak 2: Postavite React Query infinite query za paginaciju na serveru#
Cilj je dohvaćati stranice i prikazati ih kao jednu kontinuiranu listu za virtualizer.
Ovo je praktičan obrazac za cursor paginaciju s useInfiniteQuery. Držite query key stabilnim i uključite sortiranje i filtre kako bi cache unosi bili ispravni.
import { useInfiniteQuery } from '@tanstack/react-query';
type RowItem = { id: string; name: string; status: string; updatedAt: string };
type Page = { items: RowItem[]; nextCursor: string | null };
function useGridData(params: {
sort: string;
filters: Record<string, string | undefined>;
q: string;
limit: number;
}) {
return useInfiniteQuery({
queryKey: ['grid', params],
initialPageParam: null as string | null,
queryFn: async ({ pageParam }) => {
const url = new URL('/api/items', window.location.origin);
url.searchParams.set('sort', params.sort);
url.searchParams.set('q', params.q);
url.searchParams.set('limit', String(params.limit));
if (pageParam) url.searchParams.set('cursor', pageParam);
for (const [k, v] of Object.entries(params.filters)) {
if (v) url.searchParams.set(`filters[${k}]`, v);
}
const res = await fetch(url.toString());
if (!res.ok) throw new Error('Failed to load');
return (await res.json()) as Page;
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 30_000,
});
}Poravnajte stranice za tablicu#
TanStack Table očekuje array. Poravnajte stranice i držite to memoizirano.
import { useMemo } from 'react';
function useFlatRows(data: { pages?: { items: RowItem[] }[] } | undefined) {
return useMemo(() => {
const pages = data?.pages ?? [];
return pages.flatMap((p) => p.items);
}, [data]);
}Za veće aplikacije tretirajte cache i invalidation kao dizajnersku brigu prve klase. Najčešći problemi pri skaliranju su “refetch oluje”, netočni query keyjevi i mutation odgovori koji ne ažuriraju pravi cache unos. Pogledajte React Query invalidacija cachea, paginacija i mutacije na velikoj skali.
# Korak 3: Konfigurirajte TanStack Table za sortiranje i filtriranje na serveru#
Kad sortirate i filtrirate na serveru, morate isključiti client-side row modele za te operacije. TanStack Table to uredno podržava.
Minimalna postava tablice#
| Značajka tablice | Postavka | Zašto |
|---|---|---|
| Sorting | manualSorting: true | Server je vlasnik poretka |
| Filtering | manualFiltering: true | Server je vlasnik rezultata filtera |
| Pagination | rješava React Query | Infinite stranice, ne paging tablice |
Primjer konfiguracije:
import {
getCoreRowModel,
useReactTable,
type ColumnDef,
type SortingState,
type ColumnFiltersState,
} from '@tanstack/react-table';
function useServerTable(args: {
data: RowItem[];
columns: ColumnDef<RowItem>[];
sorting: SortingState;
columnFilters: ColumnFiltersState;
onSortingChange: (s: SortingState) => void;
onColumnFiltersChange: (f: ColumnFiltersState) => void;
}) {
return useReactTable({
data: args.data,
columns: args.columns,
state: {
sorting: args.sorting,
columnFilters: args.columnFilters,
},
onSortingChange: args.onSortingChange,
onColumnFiltersChange: args.onColumnFiltersChange,
getCoreRowModel: getCoreRowModel(),
manualSorting: true,
manualFiltering: true,
});
}Pretvaranje TanStack stanja u API parametre#
Mapiranje neka bude predvidljivo. Na primjer, dopustite jedno aktivno sortiranje.
function sortingToSortParam(sorting: { id: string; desc: boolean }[]) {
if (!sorting?.length) return 'updatedAt:desc';
const s = sorting[0];
return `${s.id}:${s.desc ? 'desc' : 'asc'}`;
}💡 Savjet: Držite server sort ključeve jednakima column accessor ključevima. Svaki sloj “prevođenja” je još jedno mjesto za bugove i još jedan razlog da sinkronizacija s URL-om postane krhka.
# Korak 4: Dodajte TanStack Virtual za virtualizaciju redaka#
Virtualizirate renderirane retke, ne dohvaćene stranice. Infinite query može držati tisuće itema, dok DOM drži samo vidljivi izrez.
Ključno je izmjeriti scroll container i pitati virtualizer koje indexe treba renderirati.
Postava virtualizera#
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function useRowVirtualizer(rowCount: number) {
const parentRef = useRef<HTMLDivElement | null>(null);
const rowVirtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => 44,
overscan: 8,
});
return { parentRef, rowVirtualizer };
}Renderiranje virtualnih redaka s apsolutnim pozicioniranjem#
Ovaj obrazac drži skrolanje nativnim i glatkim.
const virtualItems = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
// Inside render:
<div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
<div style={{ height: totalSize, position: 'relative' }}>
{virtualItems.map((v) => {
const row = table.getRowModel().rows[v.index];
return (
<div
key={row.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${v.start}px)`,
}}
>
{/* render cells */}
</div>
);
})}
</div>
</div>Varijabilne visine redaka#
Ako se retci mogu prelamati po tekstu ili proširivati, prebacite se na sizing temeljen na mjerenju. Možete izmjeriti svaki red i pozvati rowVirtualizer.measureElement.
Ako želite stabilne performanse, držite visine redaka fiksnima za “normalno” stanje, a proširenje prebacite u detail panel izvan virtualizirane liste.
ℹ️ Napomena: Virtualizacija i sticky headeri su kompatibilni, ali najlakše je kada je header izvan scroll containera, a virtualizira se samo body.
# Korak 5: Okidač beskonačnog skrolanja na temelju virtualizer raspona#
Ne okidajte fetch na temelju scroll pixel vrijednosti. Okidajte na temelju “blizu kraja renderiranog raspona”.
Kad se indeks zadnjeg virtualnog retka približi duljini učitanih redaka, dohvatite sljedeću stranicu.
import { useEffect } from 'react';
function useFetchNextOnScroll(args: {
virtualItems: { index: number }[];
rowsLength: number;
hasNextPage: boolean;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
}) {
useEffect(() => {
const last = args.virtualItems[args.virtualItems.length - 1];
if (!last) return;
const threshold = 15;
const shouldLoad =
last.index >= args.rowsLength - 1 - threshold &&
args.hasNextPage &&
!args.isFetchingNextPage;
if (shouldLoad) args.fetchNextPage();
}, [
args.virtualItems,
args.rowsLength,
args.hasNextPage,
args.isFetchingNextPage,
args.fetchNextPage,
]);
}Reset pri promjeni “sort ili filter”#
Kad se promijeni sort ili filteri, želite:
- 1Resetirati na prvu stranicu.
- 2Skrolati na vrh.
- 3Izbjeći prikaz starih itema pomiješanih s novim poretkom.
To uglavnom dobivate “besplatno” ako query key uključuje sort i filtre, jer React Query napravi novi cache unos i stare stranice se neće nadodavati u novi query.
Također eksplicitno skrolajte na vrh:
function scrollToTop(parent: HTMLDivElement | null) {
if (parent) parent.scrollTo({ top: 0 });
}# Korak 6: Sačuvajte stanje selekcije kroz stranice, refetcheve i sortiranje#
Selekcija puca kada je vezana uz indeks retka. Mora biti vezana uz stabilan ID retka.
Spremite selekciju u Set keyan po row.id, neovisno o trenutno učitanim stranicama.
Obrazac za selection store#
| Odluka | Preporučeno | Zašto |
|---|---|---|
| Key po | id | Stabilno kroz paginaciju i sortiranje |
| Pohrana | useState ili vanjski store | Ovisi koliko je globalno |
| Default | prazno | Izbjegava iznenađenja tipa “select all” |
| Bulk akcije | rade nad ID-jevima | Server može primiti liste ID-jeva |
Primjer:
import { useCallback, useMemo, useState } from 'react';
function useRowSelection() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
const toggle = useCallback((id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
const isSelected = useCallback(
(id: string) => selectedIds.has(id),
[selectedIds]
);
const selectedCount = selectedIds.size;
return { selectedIds, toggle, isSelected, selectedCount, setSelectedIds };
}Odaberi sve vidljivo vs odaberi sve filtrirano#
Budite eksplicitni u UI-ju, jer to znače različite stvari.
| Akcija | Što odabire | Implementacija |
|---|---|---|
| Select visible | Samo učitane stavke | Dodajte ID-jeve iz poravnatih redaka |
| Select all filtered | Cijeli server rezultat | Server-side flag ili selekcija temeljena na queryju |
Za “select all filtered” nemojte povlačiti sve ID-jeve na klijent. Umjesto toga koristite server-side bulk operaciju koja se primjenjuje na trenutni filter query, a na klijentu držite skup “izuzetih ID-jeva” ako trebate iznimke.
⚠️ Upozorenje: “Select all” na beskonačnom skrolanju često postane skriveni bug performansi gdje klijent pokušava akumulirati desetke tisuća ID-jeva. Preferirajte server-side bulk operacije.
# Korak 7: URL sinkronizacija za sortiranje, filtre i pretragu#
URL sinkronizacija čini grid dijeljivim i čuva stanje kroz refresh. Također smanjuje interne bugove stanja jer imate jedan izvor istine.
Što staviti u URL#
| Stanje | Staviti u URL | Napomena |
|---|---|---|
Globalna pretraga q | Da | Debounce ažuriranja |
| Filteri stupaca | Da | Samo serijalizabilne primitive |
| Sorting | Da | Koristite col:dir |
| Cursor | Obično ne | Cursori su efemerni; reset na reload |
| Odabrani ID-jevi | Ne | Preveliko i osjetljivo za privatnost |
Helperi za query string#
Kodiranje držite jednostavnim. Koristite stringove odvojene zarezom, ne JSON.
function encodeFilters(filters: Record<string, string | undefined>) {
return Object.entries(filters)
.filter(([, v]) => Boolean(v))
.map(([k, v]) => `${k}:${encodeURIComponent(String(v))}`)
.join(',');
}
function decodeFilters(value: string) {
const out: Record<string, string> = {};
if (!value) return out;
for (const part of value.split(',')) {
const [k, v] = part.split(':');
if (k && v) out[k] = decodeURIComponent(v);
}
return out;
}Debounced ažuriranja URL-a#
Ažurirajte URL kad korisnik prestane tipkati, ne na svaki pritisak tipke. Debouncing također smanjuje query churn jer query keyjevi često ovise o tim parametrima.
Ako niste sigurni dolaze li rerenderi iz URL stanja ili stanja tablice, profilirajte. Najčešća greška je “stanje spremljeno na previše mjesta”, što prisiljava dodatne rendere i razbija memoizaciju. Obrasci u profiliranju performansi u Reactu, memoizaciji i obrascima renderiranja pomažu vam identificirati točnu komponentu koja uzrokuje churn.
# Korak 8: Optimistična ažuriranja za izmjene i grupne akcije#
Optimistična ažuriranja čine grid trenutačnim. Ključ je ažurirati svaku stranicu u cacheu infinite queryja, ne samo trenutno vidljivi izrez.
Ažuriranje stavke kroz stranice#
Ova mutacija ažurira cachirane infinite query podatke tako da mapira kroz stranice.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateItem(params: { sort: string; filters: any; q: string; limit: number }) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: { id: string; patch: Partial<RowItem> }) => {
const res = await fetch(`/api/items/${input.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input.patch),
});
if (!res.ok) throw new Error('Update failed');
return (await res.json()) as RowItem;
},
onMutate: async (input) => {
const key = ['grid', params] as const;
await qc.cancelQueries({ queryKey: key });
const previous = qc.getQueryData<any>(key);
qc.setQueryData(key, (old: any) => {
if (!old?.pages) return old;
return {
...old,
pages: old.pages.map((p: any) => ({
...p,
items: p.items.map((it: RowItem) =>
it.id === input.id ? { ...it, ...input.patch } : it
),
})),
};
});
return { previous, key };
},
onError: (_err, _input, ctx) => {
if (ctx?.previous) qc.setQueryData(ctx.key, ctx.previous);
},
onSettled: (_data, _err, _input, ctx) => {
if (ctx?.key) qc.invalidateQueries({ queryKey: ctx.key });
},
});
}Optimistični delete uz čišćenje selekcije#
Nakon brisanja uklonite ID iz selekcije i uklonite ga iz cachiranih stranica.
| Korak | Akcija | Zašto |
|---|---|---|
| 1 | Optimistički ukloniti iz cachea | Trenutačan UI |
| 2 | Ukloniti iz selection seta | Sprječava “ghost” selekciju |
| 3 | Invalidirati query | Uskladiti sa serverom |
Kod mutacija držite malim i predvidljivim. Za više obrazaca, uključujući pagination-aware pravila invalidacije, pogledajte React Query invalidacija cachea, paginacija i mutacije na velikoj skali.
💡 Savjet: Ako izmjena promijeni sort poredak, optimistična ažuriranja mogu privremeno prikazati redak na “krivoj” poziciji. Za sortirajuće stupce preferirajte brzu invalidaciju nakon mutate, ili primijenite optimističnu izmjenu i prihvatite kratko neslaganje do refetcha.
# Česte zamke i kako ih izbjeći#
- 1
Keyanje redaka po indeksu
Svugdje koristite stabilneidključeve. Ključevi po indeksu razbijaju selekciju, re-use u virtualizaciji i mogu stvoriti vizualne glitchove pri sortiranju. - 2
Miješanje client-side i server-side sortiranja
Ako server sortira, isključite client sort row modele i držite sorting state tablice čisto kao server parametar. - 3
Okidanje fetcha direktno na scroll evente
Scroll eventi se pale vrlo često. Koristite virtualizer raspon da odlučite kada ste blizu kraja. - 4
Neograničeni query keyjevi
Ne stavljajte ne-serijalizabilne objekte u query keyjeve. Parametre držite stabilnima i plitkima da caching radi. - 5
Renderiranje teških cell komponenti unutar virtualizirane liste
Virtualizacija smanjuje DOM, ali skupi renderi ćelija i dalje bole. Memoizirajte cell renderere i izbjegavajte closure po ćeliji gdje god možete. Koristite workflow profiliranja u profiliranju performansi u Reactu, memoizaciji i obrascima renderiranja.
# Ključne poruke#
- Koristite virtualizaciju React tablica kako biste ograničili veličinu DOM-a na mali vidljivi prozor, čak i kada su podaci u desecima tisuća.
- Tretirajte beskonačno skrolanje kao UI sloj iznad cursor paginacije na serveru koristeći React Query infinite queryje.
- Implementirajte sortiranje i filtriranje na serveru s
manualSortingimanualFiltering, te mapirajte stanje tablice u eksplicitne API parametre. - Očuvajte selekciju tako da je spremate u Set stabilnih ID-jeva redaka, nikad po indeksu retka, i rješavajte “select all filtered” na serveru.
- Sinkronizirajte pretragu, filtre i sortiranje u URL query string kako bi grid bio dijeljiv i otporan kroz reloade.
- Koristite optimistična ažuriranja tako da patchate svaku stranicu u cacheu infinite queryja, a zatim invalidirate kako biste se uskladili s istinom servera.
# Zaključak#
Brz data grid je sustav: virtualizacija za renderiranje, paginacija i caching za podatke te predvidivo upravljanje stanjem za selekciju, URL sinkronizaciju i mutacije.
Ako želite da Samioda implementira produkcijski TanStack grid u vašoj React ili Next.js aplikaciji, uključujući filtriranje i sortiranje na serveru, stanje sinkronizirano s URL-om i pouzdana optimistična ažuriranja, kontaktirajte nas putem samioda.com i podijelite veličinu dataseta, potrebne stupce i backend ograničenja.
FAQ
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Web razvoj
Sve →Next.js real-time značajke: WebSockets vs SSE vs Supabase Realtime (kada što koristiti)
Praktična usporedba Next.js opcija za real-time—WebSockets, Server-Sent Events i Supabase Realtime—s fokusom na ograničenja hostinga, skalabilnost, autentikaciju, trošak i što koristiti za chat, nadzorne ploče, obavijesti i suradničko uređivanje.
Next.js ograničavanje stope zahtjeva i zaštita od botova: obrasci za API-je, Server Actions i Edge (vodič za 2026.)
Praktični obrasci za ograničavanje stope zahtjeva u Next.js-u za Route Handlers, Server Actions i Edge runtime — uz token bucket strategije, ograničenja temeljena na Redis-u, WAF/CDN pravila, nadzor i ublažavanje lažno pozitivnih blokiranja.
Next.js SaaS kontrolna lista za onboarding: računi, dozvole, emailovi i probni periodi (App Router, 2026.)
Kontrolna lista za onboarding Next.js SaaS aplikacije spremne za produkciju, koja pokriva autentikaciju, organizacije, pozivnice, RBAC, transakcijske emailove i konverziju iz probnog u plaćeni plan uz praktične obrasce, biblioteke i uobičajene zamke.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
React Query u velikim aplikacijama: invalidacija cachea, paginacija i obrasci mutacija za stvarne aplikacije
Najbolje prakse invalidacije cachea u React Queryju za aplikacije iz stvarnog svijeta: skalabilan dizajn query keyjeva, strategija invalidacije, optimistična ažuriranja, infinite queryji i pozadinski refetch u Next.js App Routeru.
React performanse u 2026.: profiliranje, memoizacija i obrasci renderiranja koji stvarno rade
Praktičan vodič korak po korak za profiliranje performansi i memoizaciju u Reactu u 2026.: kako dijagnosticirati spora sučelja uz React DevTools Profiler i why-did-you-render, odabrati prave obrasce renderiranja i izbjeći preuranjenu optimizaciju.
Izgradnja React dizajn sustava s dizajnerskim tokenima: Tailwind CSS + Radix UI + TypeScript
Praktičan vodič za 2026. o izgradnji React dizajn sustava s Tailwindom i Radixom uz dizajnerske tokene, pristupačne primitive, tematiziranje i ponovno iskoristive pakete kroz više aplikacija.