ReactNext.jsReact Server ComponentsPerformanseWeb razvojVodič

React Server Components (RSC): Što su i kako ih koristiti u Next.js App Routeru

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

# Što ćete naučiti#

React Server Components (često se pretražuje kao react server components) jedan su od najvećih pomaka u modernoj React arhitekturi: omogućuju da dijelove UI-ja renderirate na serveru bez slanja njihovog JavaScripta u browser. To mijenja način na koji razmišljate o performansama, dohvaćanju podataka i granicama između komponenti.

Ovaj vodič objašnjava RSC koncepte jednostavnim jezikom, pokazuje kada koristiti Server vs Client Components i prolazi kroz praktične primjere u Next.js App Routeru (danas najčešća produkcijska implementacija). Ako ste novi u App Routeru, krenite s osnovnim uvodom: Kako započeti s Next.js.

# Preduvjeti#

ZahtjevVerzijaNapomena
Node.js18+ (20+ preporučeno)Potrebno za Next.js dev/build
Next.js14+App Router + RSC podrška
React18+RSC je mogućnost iz ere Reacta 18
Osnove ReactaKomponente, props, uvjetno renderiranje
Osnove HTTP/dohvata podatakaREST/GraphQL koncepti pomažu

ℹ️ Napomena: React Server Components su React značajka, ali Next.js App Router je mjesto gdje ih većina timova danas koristi. Mentalni model koji ovdje usvojite prenosi se i na druge frameworke koji podržavaju RSC.

# React Server Components: mentalni model#

Dobar način da razumijete RSC je da odvojite gdje se komponenta izvršava od onoga što korisnik vidi.

Što je Server Component?#

Server Component je React komponenta koja se izvršava isključivo na serveru. Njezin se izlaz koristi za izgradnju UI-ja, ali se njezin kod ne isporučuje u browser.

Ta jedna osobina ima dvije velike posljedice:

  1. 1
    Možete sigurno pristupati resursima koji postoje samo na serveru (DB, tajne, interne usluge).
  2. 2
    Smanjujete količinu JavaScripta koju browser mora preuzeti, parsirati i izvršiti.

Što je Client Component?#

Client Component se izvršava u browseru. Koristi se kada trebate interaktivnost: state, efekte, event handlere i browser API-je.

U Next.js App Routeru u Client Component ulazite tako da na vrh datoteke dodate direktivu "use client".

RSC nije SSR (i nije “samo API + HTML”)#

SSR (Server-Side Rendering) generira HTML na serveru za početno učitavanje. RSC proizvodi server-renderano stablo komponenti koje se može streamati i kompozicijski slagati, te omogućuje obrazac u kojem logika dohvaćanja podataka i renderiranja može živjeti “uz” vaše komponente bez izlaganja tog koda browseru.

Praktično, u Next.js App Routeru obično kombinirate:

  • Server Components za dohvat podataka + renderiranje
  • Client Components za interaktivne otoke (filteri, gumbi, forme)

🎯 Ključna poruka: Uz RSC, kao zadano koristite Server Components za renderiranje i pristup podacima, a na Client Components “prelazite” samo ondje gdje to zahtijeva korisnička interakcija.

# Zašto su React Server Components važni (benefiti koje možete mjeriti)#

RSC je primarno značajka performansi i arhitekture, a ne sintakse. Ovo su benefiti koje timovi najčešće primijete u produkciji.

1) Manji client bundle (manje JS-a se isporučuje)#

Budući da se Server Components nikad ne izvršavaju na klijentu, njihov kod nije uključen u browser bundle. To smanjuje:

  • veličinu preuzimanja (posebno na mobitelu)
  • vrijeme parsiranja/kompajliranja
  • izvršavanje JS-a na main threadu

To je važno jer performanse kod stvarnih korisnika često ograničava JS, a ne HTML. Primjerice, Googleovi Web Vitals naglašavaju responsivnost (INP) i stabilnost renderiranja (CLS). Manje isporučenog JavaScripta tipično pomaže smanjiti “long tasks” i poboljšati latenciju interakcije.

2) Jednostavniji “secure by default” pristup podacima#

Server Components mogu pristupati tajnama i internim mrežama bez curenja u browser. To uklanja cijelu klasu grešaka tipa “ups, isporučili smo API ključ”.

Primjeri sigurnog server-only rada:

  • upiti prema Postgres/MySQL
  • pozivanje internih microservicea
  • korištenje process.env.* tajni
  • potpisivanje zahtjeva
  • generiranje sigurnih tokena

3) Bolja ergonomija dohvaćanja podataka (bliže UI-ju)#

Umjesto izgradnje zasebnog “data layera” koji zrcali vaše stablo komponenti, RSC potiče dohvat ondje gdje renderirate, a da pritom sve ostaje na serveru.

U Next.js možete await-ati podatke direktno u Server Components, a zatim rezultate proslijediti dalje kao props.

4) Streaming + Suspense za brže percipirano učitavanje#

RSC odlično radi sa streamingom. Možete brzo renderirati “shell” UI, a zatim streamati sporije segmente kako se razrješavaju (liste proizvoda, preporuke, komentari).

To poboljšava percipirane performanse, posebno na sporim vezama ili kada gađate više backenda.

💡 Savjet: Ako radite eCommerce ili content-heavy stranicu, česta pobjeda je: renderirati header + naslov proizvoda odmah, a zatim streamati “slične proizvode” i “recenzije” iza <Suspense> granica.

# Server vs Client Components: okvir za odluku#

Koristite ovu tablicu da odlučite gdje komponenta treba živjeti.

ZahtjevPreferiraj Server ComponentPreferiraj Client Component
Treba useState / useEffect / useReducer
Treba event handlere (onClick, onSubmit)
Koristi browser API-je (window, document, localStorage)
Dohvaća podatke uz tajne / DB pristup
Teško renderiranje / transformacija podataka✗ (osim ako treba za UX)
SEO-kritičan sadržajPonekad (ali obično server)
Ponovno iskoristiv UI widget (čisto prezentacijski)✓ (ovisno o interaktivnosti)
Koristi third-party client-only biblioteke (charts, maps)

Praktično pravilo koje radi za većinu timova:

  • Server: stranice, layouti, komponente za učitavanje podataka, sekcije sadržaja, liste
  • Client: inputi, togglei, modali, toastovi, kompleksne interakcije

⚠️ Upozorenje: Client Component granica povlači svu importanu djecu u client bundle. Ako dodate "use client" previsoko u stablu (npr. u layout.tsx), možete slučajno prisiliti velike dijelove aplikacije da postanu client-side.

# Next.js App Router: RSC kao zadano ponašanje#

U Next.js direktoriju app/:

  • page.tsx datoteke su Server Components po defaultu
  • layout.tsx datoteke su Server Components po defaultu
  • Client ponašanje uključujete pomoću "use client"

To potiče arhitekturu u kojoj se većina renderiranja događa na serveru, a samo interaktivni dijelovi postaju client “otoci”.

Primjer strukture projekta#

PutanjaTipična ulogaServer/Client
app/products/page.tsxUlaz rute, dohvat podatakaServer
app/products/ProductGrid.tsxRender liste proizvodaServer
app/products/Filters.tsxInteraktivni UI za filtereClient
app/products/ProductCard.tsxPrezentacijska kartica stavkeServer (osim ako je interaktivna)

# Korak 1: Napravite Server Component stranicu koja dohvaća podatke#

Ovaj primjer pokazuje Server Component page.tsx koji dohvaća proizvode i renderira ih. U stvarnim aplikacijama koristili biste DB ili interni API; ovdje ćemo koristiti placeholder endpoint.

TSX
// app/products/page.tsx
type Product = { id: string; name: string; price: number };
 
async function getProducts(): Promise<Product[]> {
  const res = await fetch("https://example.com/api/products", {
    // In Next.js, fetch is server-aware; use caching intentionally.
    cache: "no-store",
  });
 
  if (!res.ok) throw new Error("Failed to fetch products");
  return res.json();
}
 
export default async function ProductsPage() {
  const products = await getProducts();
 
  return (
    <main>
      <h1>Products</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>
            {p.name} — €{p.price}
          </li>
        ))}
      </ul>
    </main>
  );
}

Zašto je ovo bitno: browser dobiva HTML (i RSC payload), ali ne dobiva kod za getProducts() niti vaše server tajne (ako ih ima). To je čista separacija između UI-ja i pristupa podacima.

# Korak 2: Dodajte Client Component za interaktivnost (Filteri)#

Filtriranje je interaktivno: treba state i event handlere. Držite ga na klijentu, ali nemojte cijelu stranicu prebaciti na klijent.

Client komponenta: Filters.tsx#

TSX
// app/products/Filters.tsx
"use client";
 
import { useMemo, useState } from "react";
 
type Props = {
  categories: string[];
  onChange: (category: string | null) => void;
};
 
export default function Filters({ categories, onChange }: Props) {
  const [selected, setSelected] = useState<string | null>(null);
 
  const options = useMemo(() => ["All", ...categories], [categories]);
 
  return (
    <div>
      {options.map((c) => {
        const value = c === "All" ? null : c;
        return (
          <button
            key={c}
            onClick={() => {
              setSelected(value);
              onChange(value);
            }}
            aria-pressed={selected === value}
          >
            {c}
          </button>
        );
      })}
    </div>
  );
}

Server komponenta stranice koristi client komponentu#

U App Routeru, Server Component može renderirati Client Component i proslijediti serijalizabilne props (stringove, brojeve, nizove, plain objekte).

TSX
// app/products/page.tsx
import Filters from "./Filters";
 
export default async function ProductsPage() {
  const categories = ["Shoes", "T-Shirts", "Accessories"];
 
  return (
    <main>
      <h1>Products</h1>
      <Filters
        categories={categories}
        onChange={() => {
          // You can't pass functions from server to client.
          // We'll handle this properly in the next step.
        }}
      />
    </main>
  );
}

Ovo još neće raditi jer ne možete proslijediti funkciju iz Server Componenta u Client Component.

Pa kako povezati filtere sa server-side podacima? Postoje tri uobičajena obrasca:

  1. 1
    URL search params (preporučeno za liste koje se mogu dijeliti i filtrirati)
  2. 2
    Server Actions (dobro za mutacije)
  3. 3
    Client-side fetching (samo kada je stvarno potrebno)

Sljedeće ćemo implementirati URL obrazac.

# Korak 3: Koristite URL Search Params kao most između client UI-ja i server podataka#

Kad filteri ažuriraju URL, ruta se ponovno renderira na serveru s novim searchParams. Tako dohvat podataka ostaje na serveru, a stranica je “shareable” i “bookmarkable”.

Client komponenta ažurira URL#

TSX
// app/products/Filters.tsx
"use client";
 
import { useRouter, useSearchParams } from "next/navigation";
 
type Props = { categories: string[] };
 
export default function Filters({ categories }: Props) {
  const router = useRouter();
  const params = useSearchParams();
  const active = params.get("category");
 
  return (
    <div>
      <button
        onClick={() => router.push("/products")}
        aria-pressed={!active}
      >
        All
      </button>
 
      {categories.map((c) => (
        <button
          key={c}
          onClick={() => router.push(`/products?category=${encodeURIComponent(c)}`)}
          aria-pressed={active === c}
        >
          {c}
        </button>
      ))}
    </div>
  );
}

Server komponenta koristi searchParams za server-side filtriranje#

TSX
// app/products/page.tsx
type Product = { id: string; name: string; price: number; category: string };
 
async function getProducts(category?: string | null): Promise<Product[]> {
  const url = new URL("https://example.com/api/products");
  if (category) url.searchParams.set("category", category);
 
  const res = await fetch(url.toString(), { cache: "no-store" });
  if (!res.ok) throw new Error("Failed to fetch products");
  return res.json();
}
 
export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<{ category?: string }>;
}) {
  const { category } = await searchParams;
  const products = await getProducts(category);
 
  return (
    <main>
      <h1>Products</h1>
      <p>Category: {category ?? "All"}</p>
      <ul>
        {products.map((p) => (
          <li key={p.id}>
            {p.name} — €{p.price}
          </li>
        ))}
      </ul>
    </main>
  );
}

Zašto je ovo bitno: dobivate interaktivnost bez odustajanja od server-side dohvaćanja podataka, i izbjegavate isporučivanje vaše logike dohvaćanja na klijent.

# Korak 4: Koristite Suspense + Streaming za “teške” sekcije#

RSC dolazi do izražaja kada spore dijelove stranice podijelite u sekcije koje se učitavaju neovisno. U Next.js App Routeru to možete napraviti pomoću Suspense.

Primjer: streamajte preporuke odvojeno#

TSX
// app/products/Recommendations.tsx
type Product = { id: string; name: string };
 
async function getRecommendations(): Promise<Product[]> {
  const res = await fetch("https://example.com/api/recommendations", {
    cache: "no-store",
  });
  if (!res.ok) throw new Error("Failed to fetch recommendations");
  return res.json();
}
 
export default async function Recommendations() {
  const items = await getRecommendations();
 
  return (
    <aside>
      <h2>Recommended</h2>
      <ul>
        {items.map((p) => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </aside>
  );
}
TSX
// app/products/page.tsx
import { Suspense } from "react";
import Recommendations from "./Recommendations";
 
export default async function ProductsPage() {
  return (
    <main>
      <h1>Products</h1>
 
      <Suspense fallback={<p>Loading recommendations…</p>}>
        <Recommendations />
      </Suspense>
    </main>
  );
}

Ovo je važno jer korisnici mogu početi čitati i koristiti glavni sadržaj dok se sekundarni sadržaj streama, što često poboljšava percipiranu brzinu na sporijim mrežama.

💡 Savjet: Najsporije backend pozive stavite iza Suspense granice, a sadržaj iznad pregiba (above-the-fold) držite u brzoj Server Componenti. Ovo obično poboljšava realni engagement više nego mikro-optimizacija client-side koda.

# Korak 5: Server Actions (kada trebate mutacije)#

Filtriranje je najbolje raditi preko URL parametara, ali slanje formi i mutacije često su čišće uz Server Actions. Omogućuju da pokrenete server kod iz forme bez izrade zasebne API rute.

Primjer: add-to-wishlist action#

TSX
// app/products/actions.ts
"use server";
 
export async function addToWishlist(productId: string) {
  // Safe: server-only logic (DB call, internal API, auth checks)
  await fetch("https://example.com/api/wishlist", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ productId }),
  });
}
TSX
// app/products/AddToWishlistButton.tsx
"use client";
 
import { useTransition } from "react";
import { addToWishlist } from "./actions";
 
export default function AddToWishlistButton({ productId }: { productId: string }) {
  const [pending, startTransition] = useTransition();
 
  return (
    <button
      disabled={pending}
      onClick={() => startTransition(() => addToWishlist(productId))}
    >
      {pending ? "Saving…" : "Add to wishlist"}
    </button>
  );
}

Zašto je ovo bitno: logika mutacije ostaje na serveru, ne isporučujete credentiale i smanjujete API boilerplate. Koristite ovo za write operacije: create/update/delete, pretplate, kontakt forme itd.

⚠️ Upozorenje: Nemojte tretirati Server Actions kao “besplatnu sigurnost”. I dalje trebate autentikaciju, autorizaciju i validaciju inputa na serveru, isto kao i za API endpoint.

# Česta RSC ograničenja (i kako dizajnirati oko njih)#

RSC je moćan, ali ima oštre rubove. Većina produkcijskih problema dolazi iz pogrešnog razumijevanja ograničenja.

1) Ne možete koristiti hookove u Server Components#

Hookovi poput useState, useEffect i useRef zahtijevaju browser runtime. Ako ih trebate, izolirajte taj dio u Client Component.

2) Ne možete proslijediti funkcije iz Server u Client Components#

Server Components mogu prosljeđivati samo serijalizabilne props. Umjesto toga koristite jedan od ovih obrazaca:

  • URL parametre za filtriranje/sortiranje/paginaciju
  • Server Actions za mutacije
  • Client fetch za potpuno client-driven UX (zadnja opcija)

3) Client granice mogu slučajno napuhati bundle#

"use client" na krivom mjestu može pretvoriti velike dijelove u client kod. Držite client komponente male i što bliže listovima (leaf-level).

4) Third-party biblioteke mogu prisiliti Client Components#

Mnoge UI biblioteke ovise o browser API-jima. Omotajte ih u male client komponente i u njih proslijedite server-dohvaćene podatke kao props.

# Praktičan arhitekturni obrazac: “Server shell + Client islands”#

Ovo je obrazac koji dobro radi za dashboarde, marketplaceove i SaaS:

  1. 1
    Stranica i glavne sekcije su Server Components
  2. 2
    Svaki interaktivni widget je Client Component
  3. 3
    Dohvat podataka se radi na serveru, zatim se prosljeđuje dolje
Dio stranicePrimjerTip komponente
Header, navigacija, breadcrumbsStatični ili user-aware UIServer
Redovi tablice podataka100–5.000 redova server-renderano s paginacijomServer
UI za sortiranje stupacaGrupa gumba, dropdownClient
Akcije po retku“Edit”, “Archive”, modalClient
MutacijeCreate/update/deleteServer Actions

Ovo je važno jer možete zadržati nizak Time to Interactive, a istovremeno isporučiti bogat UX.

Ako gradite React/Next.js proizvod i želite da se ovaj obrazac implementira čisto uz performance budžete i analitiku, upravo to radimo u Samioda: web & mobile development.

# Caching i revalidacija: nemojte si slučajno napraviti DDOS#

Uz App Router, fetch() se integrira s Nextovim cachingom i može se revalidirati. Trebate svjesnu strategiju, inače ćete ili posluživati zastario sadržaj ili preopteretiti backend.

Uobičajeni caching modovi u Next.js (konceptualno)#

Use casePreporučena postavkaZašto
Uvijek svježi podaci (cijene, zalihe)cache: "no-store"Izbjegnite stale rezultate
Uglavnom statičan sadržaj (blog, dokumentacija)default caching ili revalidateSmanjite opterećenje backenda
“Dovoljno svježi” podaci (ažuriranja kataloga)revalidate svakih X sekundiBalans svježine i troška

Jednostavno pravilo: ako prikaz zastarjelih podataka može naštetiti korisniku (kriva cijena, kriva dostupnost), kao zadano koristite no-store ili kratku revalidaciju.

# Česte zamke (i kako ih izbjeći)#

  1. 1
    Označavanje cijelih ruta kao client — Držite "use client" na najmanjoj mogućoj granici komponente kako biste izbjegli bloat bundlea.
  2. 2
    Dohvat na klijentu kao default — Preferirajte server dohvat za inicijalni render; na klijent prebacite samo za UX koji se uživo ažurira.
  3. 3
    Pokušaj korištenja browser API-ja u Server Components — Ako trebate localStorage ili window, izolirajte tu logiku u Client Component.
  4. 4
    Ignoriranje streaming prilika — Koristite <Suspense> oko sporih sekcija poput preporuka, recenzija ili analytics widgeta.
  5. 5
    Miješanje logike mutacija u client kod — Preferirajte Server Actions za write operacije kako bi sigurnosne provjere ostale na serveru.

Za dobru App Router osnovu prije refaktora prema RSC-first obrascima, pogledajte: Kako započeti s Next.js.

# Ključne poruke#

  • Kao zadano koristite Server Components za dohvat podataka, renderiranje i siguran pristup tajnama/DB-u; isporučite manje JavaScripta u browser.
  • Client Components koristite samo za interaktivnost (state, efekti, handleri) i držite ih malima kako biste kontrolirali veličinu bundlea.
  • Povežite client UI sa server podacima pomoću URL search params za filtre/sortiranje/paginaciju; koristite Server Actions za mutacije.
  • Poboljšajte percipirane performanse streamanjem sporih sekcija iza Suspense granica umjesto blokiranja cijele stranice.
  • Caching tretirajte kao produktnu odluku: no-store za kritičnu svježinu, revalidacija za sadržaj koji tolerira blagu zastarjelost.

# Zaključak#

React Server Components mijenjaju default: vaša React aplikacija može biti server-first, sigurna po defaultu i značajno “lakša” po pitanju client JavaScripta, a i dalje podržavati interaktivan UX kroz male client otoke. U Next.js App Routeru danas dobivate produkcijski spreman RSC workflow tako da stranice i podatke držite na serveru, a u "use client" ulazite samo ondje gdje se to isplati.

Ako želite pomoć oko dizajna RSC arhitekture, performance budžeta i plana migracije (App Router, Server Actions, streaming, analitika), javite se Samioda: web & mobile development.

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.